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 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.
@@ -0,0 +1,3 @@
1
+ include README.md
2
+ include LICENSE
3
+ recursive-include tests *.py
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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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"