gesto 0.1.0__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.
- gesto-0.1.0/LICENSE +21 -0
- gesto-0.1.0/MANIFEST.in +3 -0
- gesto-0.1.0/PKG-INFO +160 -0
- gesto-0.1.0/README.md +127 -0
- gesto-0.1.0/pyproject.toml +55 -0
- gesto-0.1.0/setup.cfg +4 -0
- gesto-0.1.0/src/gesto/__init__.py +71 -0
- gesto-0.1.0/src/gesto/artifacts.py +132 -0
- gesto-0.1.0/src/gesto/cli.py +195 -0
- gesto-0.1.0/src/gesto/data.py +113 -0
- gesto-0.1.0/src/gesto/detect.py +191 -0
- gesto-0.1.0/src/gesto/regions.py +216 -0
- gesto-0.1.0/src/gesto/train.py +171 -0
- gesto-0.1.0/src/gesto.egg-info/PKG-INFO +160 -0
- gesto-0.1.0/src/gesto.egg-info/SOURCES.txt +20 -0
- gesto-0.1.0/src/gesto.egg-info/dependency_links.txt +1 -0
- gesto-0.1.0/src/gesto.egg-info/entry_points.txt +2 -0
- gesto-0.1.0/src/gesto.egg-info/requires.txt +9 -0
- gesto-0.1.0/src/gesto.egg-info/top_level.txt +1 -0
- gesto-0.1.0/tests/test_artifacts.py +51 -0
- gesto-0.1.0/tests/test_data.py +41 -0
- gesto-0.1.0/tests/test_regions.py +77 -0
gesto-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sundar Balamurugan
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
gesto-0.1.0/MANIFEST.in
ADDED
gesto-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: gesto
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Train and run gesture recognition models from Gesto Labeller datasets
|
|
5
|
+
Author: Sundar Balamurugan
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/TheMadrasTechie/gesto
|
|
8
|
+
Project-URL: Issues, https://github.com/TheMadrasTechie/gesto/issues
|
|
9
|
+
Keywords: gesture-recognition,mediapipe,pose-estimation,computer-vision,lstm,sign-language
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Intended Audience :: Science/Research
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering :: Image Recognition
|
|
21
|
+
Requires-Python: >=3.9
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Requires-Dist: numpy>=1.23
|
|
25
|
+
Requires-Dist: tensorflow>=2.15
|
|
26
|
+
Requires-Dist: opencv-python>=4.8
|
|
27
|
+
Requires-Dist: mediapipe>=0.10
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
30
|
+
Requires-Dist: build; extra == "dev"
|
|
31
|
+
Requires-Dist: twine; extra == "dev"
|
|
32
|
+
Dynamic: license-file
|
|
33
|
+
|
|
34
|
+
# gesto
|
|
35
|
+
|
|
36
|
+
Train and run gesture recognition models from [Gesto Labeller](https://github.com/TheMadrasTechie) datasets.
|
|
37
|
+
|
|
38
|
+
Point it at a project folder, pick a mode and a region, and it handles the rest —
|
|
39
|
+
loading, training, versioned model storage, and live detection that matches how
|
|
40
|
+
the data was captured.
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pip install gesto
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Two modes
|
|
47
|
+
|
|
48
|
+
| mode | the gesture is… | model | example |
|
|
49
|
+
|---|---|---|---|
|
|
50
|
+
| `static` | a held shape or posture | Dense network | thumbs up, alphabet letters, a stance |
|
|
51
|
+
| `sequence` | a motion over time | stacked LSTM | waving, clapping, jogging |
|
|
52
|
+
|
|
53
|
+
Static needs far less data (every captured frame is a training sample) and
|
|
54
|
+
predicts instantly with no warm-up. Reach for `sequence` only when two gestures
|
|
55
|
+
share the same shape and differ by movement.
|
|
56
|
+
|
|
57
|
+
## Five regions
|
|
58
|
+
|
|
59
|
+
| region | dim | tracks |
|
|
60
|
+
|---|---|---|
|
|
61
|
+
| `hands_one` | 63 | one hand, 21 joints |
|
|
62
|
+
| `hands_two` | 126 | both hands |
|
|
63
|
+
| `pose` | 132 | full body, 33 points |
|
|
64
|
+
| `legs` | 32 | lower body, 8 points |
|
|
65
|
+
| `full` | 258 | body + both hands |
|
|
66
|
+
|
|
67
|
+
The region must match how the project was captured — `gesto` checks the feature
|
|
68
|
+
dimension and tells you if it doesn't.
|
|
69
|
+
|
|
70
|
+
## Command line
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
# what's in this dataset?
|
|
74
|
+
gesto inspect ./gesto_projects/signs
|
|
75
|
+
|
|
76
|
+
# train
|
|
77
|
+
gesto train static hands_one ./gesto_projects/signs
|
|
78
|
+
gesto train sequence pose ./gesto_projects/jogging --seq-len 30
|
|
79
|
+
|
|
80
|
+
# detect (newest model by default)
|
|
81
|
+
gesto detect static hands_one
|
|
82
|
+
gesto detect sequence pose --source clip.mp4
|
|
83
|
+
gesto detect static hands_one --version 2
|
|
84
|
+
|
|
85
|
+
# what have I trained?
|
|
86
|
+
gesto list
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Python
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
import gesto
|
|
93
|
+
|
|
94
|
+
run = gesto.train("./gesto_projects/signs", region="hands_one", mode="static")
|
|
95
|
+
gesto.detect("static", "hands_one")
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Or drive a model yourself:
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
from gesto.detect import Predictor
|
|
102
|
+
|
|
103
|
+
predictor = Predictor.load("static", "hands_one")
|
|
104
|
+
vector = predictor.features(holistic_result) # extract + normalize
|
|
105
|
+
probs = predictor.predict(vector)
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Where models go
|
|
109
|
+
|
|
110
|
+
Everything lands under one `artifacts/` folder, split by mode then region.
|
|
111
|
+
Training never overwrites an earlier run — it versions:
|
|
112
|
+
|
|
113
|
+
```
|
|
114
|
+
artifacts/
|
|
115
|
+
static/
|
|
116
|
+
hands_one/ model.keras, labels.json
|
|
117
|
+
hands_one_2/ the next run
|
|
118
|
+
pose/
|
|
119
|
+
sequence/
|
|
120
|
+
pose/
|
|
121
|
+
pose_2/
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
`gesto detect static pose` picks the newest version; `--version 1` picks a
|
|
125
|
+
specific one.
|
|
126
|
+
|
|
127
|
+
## Matching your capture
|
|
128
|
+
|
|
129
|
+
Predictions are only correct when detection feeds the model the same kind of
|
|
130
|
+
vector it trained on. `gesto` mirrors Gesto Labeller exactly:
|
|
131
|
+
|
|
132
|
+
- **the same engine** — MediaPipe Holistic, same confidence settings
|
|
133
|
+
- **the same landmark order** — including one-hand mode preferring the right
|
|
134
|
+
hand and falling back to the left
|
|
135
|
+
- **the same normalization** — translation/scale-invariant, verified identical
|
|
136
|
+
- **the same mirroring** — webcam frames are flipped, video files are not
|
|
137
|
+
|
|
138
|
+
If you captured with Gesto's **Normalise** unchecked, pass `--raw` when training
|
|
139
|
+
so detection knows to skip it.
|
|
140
|
+
|
|
141
|
+
## Getting good results
|
|
142
|
+
|
|
143
|
+
- **Balance your classes.** Similar sample counts per class; `gesto` applies
|
|
144
|
+
class weights but balanced data is better.
|
|
145
|
+
- **Enough samples.** ~20–30 static frames per class, or ~15–30 sequences.
|
|
146
|
+
Small datasets automatically get a lighter model, since an oversized network
|
|
147
|
+
on little data overfits and collapses to predicting one class.
|
|
148
|
+
- **Consistent clip length** for sequence mode — set "Max frames" in Gesto
|
|
149
|
+
Labeller so every capture is the same length.
|
|
150
|
+
|
|
151
|
+
Run `gesto inspect <project>` to check all of this before training.
|
|
152
|
+
|
|
153
|
+
## Requirements
|
|
154
|
+
|
|
155
|
+
Python 3.9+, TensorFlow, OpenCV, MediaPipe, NumPy — all installed with the
|
|
156
|
+
package.
|
|
157
|
+
|
|
158
|
+
## License
|
|
159
|
+
|
|
160
|
+
MIT
|
gesto-0.1.0/README.md
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# gesto
|
|
2
|
+
|
|
3
|
+
Train and run gesture recognition models from [Gesto Labeller](https://github.com/TheMadrasTechie) datasets.
|
|
4
|
+
|
|
5
|
+
Point it at a project folder, pick a mode and a region, and it handles the rest —
|
|
6
|
+
loading, training, versioned model storage, and live detection that matches how
|
|
7
|
+
the data was captured.
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install gesto
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Two modes
|
|
14
|
+
|
|
15
|
+
| mode | the gesture is… | model | example |
|
|
16
|
+
|---|---|---|---|
|
|
17
|
+
| `static` | a held shape or posture | Dense network | thumbs up, alphabet letters, a stance |
|
|
18
|
+
| `sequence` | a motion over time | stacked LSTM | waving, clapping, jogging |
|
|
19
|
+
|
|
20
|
+
Static needs far less data (every captured frame is a training sample) and
|
|
21
|
+
predicts instantly with no warm-up. Reach for `sequence` only when two gestures
|
|
22
|
+
share the same shape and differ by movement.
|
|
23
|
+
|
|
24
|
+
## Five regions
|
|
25
|
+
|
|
26
|
+
| region | dim | tracks |
|
|
27
|
+
|---|---|---|
|
|
28
|
+
| `hands_one` | 63 | one hand, 21 joints |
|
|
29
|
+
| `hands_two` | 126 | both hands |
|
|
30
|
+
| `pose` | 132 | full body, 33 points |
|
|
31
|
+
| `legs` | 32 | lower body, 8 points |
|
|
32
|
+
| `full` | 258 | body + both hands |
|
|
33
|
+
|
|
34
|
+
The region must match how the project was captured — `gesto` checks the feature
|
|
35
|
+
dimension and tells you if it doesn't.
|
|
36
|
+
|
|
37
|
+
## Command line
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
# what's in this dataset?
|
|
41
|
+
gesto inspect ./gesto_projects/signs
|
|
42
|
+
|
|
43
|
+
# train
|
|
44
|
+
gesto train static hands_one ./gesto_projects/signs
|
|
45
|
+
gesto train sequence pose ./gesto_projects/jogging --seq-len 30
|
|
46
|
+
|
|
47
|
+
# detect (newest model by default)
|
|
48
|
+
gesto detect static hands_one
|
|
49
|
+
gesto detect sequence pose --source clip.mp4
|
|
50
|
+
gesto detect static hands_one --version 2
|
|
51
|
+
|
|
52
|
+
# what have I trained?
|
|
53
|
+
gesto list
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Python
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
import gesto
|
|
60
|
+
|
|
61
|
+
run = gesto.train("./gesto_projects/signs", region="hands_one", mode="static")
|
|
62
|
+
gesto.detect("static", "hands_one")
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Or drive a model yourself:
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
from gesto.detect import Predictor
|
|
69
|
+
|
|
70
|
+
predictor = Predictor.load("static", "hands_one")
|
|
71
|
+
vector = predictor.features(holistic_result) # extract + normalize
|
|
72
|
+
probs = predictor.predict(vector)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Where models go
|
|
76
|
+
|
|
77
|
+
Everything lands under one `artifacts/` folder, split by mode then region.
|
|
78
|
+
Training never overwrites an earlier run — it versions:
|
|
79
|
+
|
|
80
|
+
```
|
|
81
|
+
artifacts/
|
|
82
|
+
static/
|
|
83
|
+
hands_one/ model.keras, labels.json
|
|
84
|
+
hands_one_2/ the next run
|
|
85
|
+
pose/
|
|
86
|
+
sequence/
|
|
87
|
+
pose/
|
|
88
|
+
pose_2/
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
`gesto detect static pose` picks the newest version; `--version 1` picks a
|
|
92
|
+
specific one.
|
|
93
|
+
|
|
94
|
+
## Matching your capture
|
|
95
|
+
|
|
96
|
+
Predictions are only correct when detection feeds the model the same kind of
|
|
97
|
+
vector it trained on. `gesto` mirrors Gesto Labeller exactly:
|
|
98
|
+
|
|
99
|
+
- **the same engine** — MediaPipe Holistic, same confidence settings
|
|
100
|
+
- **the same landmark order** — including one-hand mode preferring the right
|
|
101
|
+
hand and falling back to the left
|
|
102
|
+
- **the same normalization** — translation/scale-invariant, verified identical
|
|
103
|
+
- **the same mirroring** — webcam frames are flipped, video files are not
|
|
104
|
+
|
|
105
|
+
If you captured with Gesto's **Normalise** unchecked, pass `--raw` when training
|
|
106
|
+
so detection knows to skip it.
|
|
107
|
+
|
|
108
|
+
## Getting good results
|
|
109
|
+
|
|
110
|
+
- **Balance your classes.** Similar sample counts per class; `gesto` applies
|
|
111
|
+
class weights but balanced data is better.
|
|
112
|
+
- **Enough samples.** ~20–30 static frames per class, or ~15–30 sequences.
|
|
113
|
+
Small datasets automatically get a lighter model, since an oversized network
|
|
114
|
+
on little data overfits and collapses to predicting one class.
|
|
115
|
+
- **Consistent clip length** for sequence mode — set "Max frames" in Gesto
|
|
116
|
+
Labeller so every capture is the same length.
|
|
117
|
+
|
|
118
|
+
Run `gesto inspect <project>` to check all of this before training.
|
|
119
|
+
|
|
120
|
+
## Requirements
|
|
121
|
+
|
|
122
|
+
Python 3.9+, TensorFlow, OpenCV, MediaPipe, NumPy — all installed with the
|
|
123
|
+
package.
|
|
124
|
+
|
|
125
|
+
## License
|
|
126
|
+
|
|
127
|
+
MIT
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "gesto"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Train and run gesture recognition models from Gesto Labeller datasets"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "Sundar Balamurugan" }]
|
|
13
|
+
keywords = [
|
|
14
|
+
"gesture-recognition",
|
|
15
|
+
"mediapipe",
|
|
16
|
+
"pose-estimation",
|
|
17
|
+
"computer-vision",
|
|
18
|
+
"lstm",
|
|
19
|
+
"sign-language",
|
|
20
|
+
]
|
|
21
|
+
classifiers = [
|
|
22
|
+
"Development Status :: 4 - Beta",
|
|
23
|
+
"Intended Audience :: Developers",
|
|
24
|
+
"Intended Audience :: Science/Research",
|
|
25
|
+
"License :: OSI Approved :: MIT License",
|
|
26
|
+
"Programming Language :: Python :: 3",
|
|
27
|
+
"Programming Language :: Python :: 3.9",
|
|
28
|
+
"Programming Language :: Python :: 3.10",
|
|
29
|
+
"Programming Language :: Python :: 3.11",
|
|
30
|
+
"Programming Language :: Python :: 3.12",
|
|
31
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
32
|
+
"Topic :: Scientific/Engineering :: Image Recognition",
|
|
33
|
+
]
|
|
34
|
+
dependencies = [
|
|
35
|
+
"numpy>=1.23",
|
|
36
|
+
"tensorflow>=2.15",
|
|
37
|
+
"opencv-python>=4.8",
|
|
38
|
+
"mediapipe>=0.10",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
[project.optional-dependencies]
|
|
42
|
+
dev = ["pytest>=7", "build", "twine"]
|
|
43
|
+
|
|
44
|
+
[project.urls]
|
|
45
|
+
Homepage = "https://github.com/TheMadrasTechie/gesto"
|
|
46
|
+
Issues = "https://github.com/TheMadrasTechie/gesto/issues"
|
|
47
|
+
|
|
48
|
+
[project.scripts]
|
|
49
|
+
gesto = "gesto.cli:main"
|
|
50
|
+
|
|
51
|
+
[tool.setuptools.packages.find]
|
|
52
|
+
where = ["src"]
|
|
53
|
+
|
|
54
|
+
[tool.pytest.ini_options]
|
|
55
|
+
testpaths = ["tests"]
|
gesto-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""
|
|
2
|
+
gesto — train and run gesture recognition models from Gesto Labeller datasets.
|
|
3
|
+
|
|
4
|
+
Two model types:
|
|
5
|
+
static single frame; the gesture is a held shape or posture
|
|
6
|
+
sequence a window of frames; the gesture is a motion
|
|
7
|
+
|
|
8
|
+
Five regions: hands_one, hands_two, pose, legs, full.
|
|
9
|
+
|
|
10
|
+
Quick start (Python):
|
|
11
|
+
|
|
12
|
+
import gesto
|
|
13
|
+
|
|
14
|
+
run = gesto.train("./gesto_projects/signs", region="hands_one",
|
|
15
|
+
mode="static")
|
|
16
|
+
gesto.detect("static", "hands_one")
|
|
17
|
+
|
|
18
|
+
Quick start (command line):
|
|
19
|
+
|
|
20
|
+
gesto train static hands_one ./gesto_projects/signs
|
|
21
|
+
gesto detect static hands_one
|
|
22
|
+
|
|
23
|
+
Models are saved under artifacts/<mode>/<region>/, versioned so a new run never
|
|
24
|
+
overwrites an old one (pose, pose_2, pose_3, ...).
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
__version__ = "0.1.0"
|
|
30
|
+
|
|
31
|
+
from .regions import REGION_KEYS, REGION_INFO, feature_dim, extract, normalize
|
|
32
|
+
|
|
33
|
+
__all__ = [
|
|
34
|
+
"__version__",
|
|
35
|
+
"REGION_KEYS",
|
|
36
|
+
"REGION_INFO",
|
|
37
|
+
"feature_dim",
|
|
38
|
+
"extract",
|
|
39
|
+
"normalize",
|
|
40
|
+
"train",
|
|
41
|
+
"detect",
|
|
42
|
+
"Predictor",
|
|
43
|
+
"artifacts",
|
|
44
|
+
"data",
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def train(project_dir, region: str, mode: str = "static", **kwargs):
|
|
49
|
+
"""Train a model. See gesto.train.train for the full signature."""
|
|
50
|
+
from .train import train as _train
|
|
51
|
+
return _train(project_dir, region, mode, **kwargs)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def detect(mode: str, region: str, **kwargs):
|
|
55
|
+
"""Run live detection. See gesto.detect.run for the full signature."""
|
|
56
|
+
from .detect import run as _run
|
|
57
|
+
return _run(mode, region, **kwargs)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def Predictor(*args, **kwargs): # noqa: N802 - re-exported class-like helper
|
|
61
|
+
"""Load a trained model for programmatic use."""
|
|
62
|
+
from .detect import Predictor as _Predictor
|
|
63
|
+
return _Predictor(*args, **kwargs)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def __getattr__(name: str):
|
|
67
|
+
# lazy submodule access: gesto.artifacts / gesto.data without import cost
|
|
68
|
+
if name in ("artifacts", "data"):
|
|
69
|
+
import importlib
|
|
70
|
+
return importlib.import_module(f".{name}", __name__)
|
|
71
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Artifact storage.
|
|
3
|
+
|
|
4
|
+
Everything a training run produces lives under one root folder, split by mode
|
|
5
|
+
and then region:
|
|
6
|
+
|
|
7
|
+
artifacts/
|
|
8
|
+
static/
|
|
9
|
+
pose/ <- first static pose model
|
|
10
|
+
model.keras
|
|
11
|
+
labels.json
|
|
12
|
+
pose_2/ <- next one doesn't overwrite; it's versioned
|
|
13
|
+
hands_one/
|
|
14
|
+
sequence/
|
|
15
|
+
pose/
|
|
16
|
+
legs/
|
|
17
|
+
|
|
18
|
+
`new_run()` never overwrites: if `artifacts/static/pose` exists it returns
|
|
19
|
+
`pose_2`, then `pose_3`, and so on. `resolve()` finds the newest version when
|
|
20
|
+
you don't name one explicitly, so `gesto detect static pose` picks up the model
|
|
21
|
+
you just trained.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import json
|
|
27
|
+
import re
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
|
|
30
|
+
MODES = ("static", "sequence")
|
|
31
|
+
DEFAULT_ROOT = "artifacts"
|
|
32
|
+
|
|
33
|
+
_VERSION_RE = re.compile(r"^(?P<base>.+?)(?:_(?P<n>\d+))?$")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _check_mode(mode: str) -> None:
|
|
37
|
+
if mode not in MODES:
|
|
38
|
+
raise ValueError(f"mode must be one of {MODES}, got {mode!r}")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def mode_dir(root: str | Path, mode: str) -> Path:
|
|
42
|
+
_check_mode(mode)
|
|
43
|
+
return Path(root) / mode
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _version_of(name: str, base: str) -> int:
|
|
47
|
+
"""Return the version number a folder name represents for `base`.
|
|
48
|
+
|
|
49
|
+
"pose" -> 1, "pose_2" -> 2, anything else -> 0 (not a version of base).
|
|
50
|
+
"""
|
|
51
|
+
m = _VERSION_RE.match(name)
|
|
52
|
+
if not m or m.group("base") != base:
|
|
53
|
+
return 0
|
|
54
|
+
n = m.group("n")
|
|
55
|
+
return int(n) if n else 1
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def list_runs(root: str | Path, mode: str, region: str) -> list[Path]:
|
|
59
|
+
"""Every run folder for this mode+region, oldest version first."""
|
|
60
|
+
d = mode_dir(root, mode)
|
|
61
|
+
if not d.exists():
|
|
62
|
+
return []
|
|
63
|
+
runs = [(p, _version_of(p.name, region)) for p in d.iterdir() if p.is_dir()]
|
|
64
|
+
runs = [(p, v) for p, v in runs if v > 0]
|
|
65
|
+
runs.sort(key=lambda t: t[1])
|
|
66
|
+
return [p for p, _ in runs]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def new_run(root: str | Path, mode: str, region: str) -> Path:
|
|
70
|
+
"""Create and return a fresh run folder, versioning instead of overwriting.
|
|
71
|
+
|
|
72
|
+
First call -> artifacts/<mode>/<region>
|
|
73
|
+
Later calls -> artifacts/<mode>/<region>_2, _3, ...
|
|
74
|
+
"""
|
|
75
|
+
d = mode_dir(root, mode)
|
|
76
|
+
d.mkdir(parents=True, exist_ok=True)
|
|
77
|
+
existing = list_runs(root, mode, region)
|
|
78
|
+
if not existing:
|
|
79
|
+
run = d / region
|
|
80
|
+
else:
|
|
81
|
+
highest = max(_version_of(p.name, region) for p in existing)
|
|
82
|
+
run = d / f"{region}_{highest + 1}"
|
|
83
|
+
run.mkdir(parents=True, exist_ok=False)
|
|
84
|
+
return run
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def resolve(root: str | Path, mode: str, region: str,
|
|
88
|
+
version: int | str | None = None) -> Path:
|
|
89
|
+
"""Locate an existing run folder.
|
|
90
|
+
|
|
91
|
+
version=None -> newest version
|
|
92
|
+
version=2 -> artifacts/<mode>/<region>_2
|
|
93
|
+
version="pose_2" (a folder name) -> that exact folder
|
|
94
|
+
"""
|
|
95
|
+
d = mode_dir(root, mode)
|
|
96
|
+
if isinstance(version, str) and not version.isdigit():
|
|
97
|
+
run = d / version
|
|
98
|
+
if not run.exists():
|
|
99
|
+
raise FileNotFoundError(f"No such run: {run}")
|
|
100
|
+
return run
|
|
101
|
+
|
|
102
|
+
runs = list_runs(root, mode, region)
|
|
103
|
+
if not runs:
|
|
104
|
+
raise FileNotFoundError(
|
|
105
|
+
f"No {mode} model for region {region!r} under {d}. "
|
|
106
|
+
f"Train one first: gesto train {mode} {region} <project_dir>")
|
|
107
|
+
if version is None:
|
|
108
|
+
return runs[-1] # newest
|
|
109
|
+
want = int(version)
|
|
110
|
+
for p in runs:
|
|
111
|
+
if _version_of(p.name, region) == want:
|
|
112
|
+
return p
|
|
113
|
+
have = ", ".join(p.name for p in runs)
|
|
114
|
+
raise FileNotFoundError(
|
|
115
|
+
f"No version {want} for {mode}/{region}. Available: {have}")
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def save_meta(run: Path, meta: dict) -> Path:
|
|
119
|
+
path = Path(run) / "labels.json"
|
|
120
|
+
path.write_text(json.dumps(meta, indent=2), encoding="utf-8")
|
|
121
|
+
return path
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def load_meta(run: Path) -> dict:
|
|
125
|
+
path = Path(run) / "labels.json"
|
|
126
|
+
if not path.exists():
|
|
127
|
+
raise FileNotFoundError(f"No labels.json in {run}")
|
|
128
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def model_path(run: Path) -> Path:
|
|
132
|
+
return Path(run) / "model.keras"
|