framepin 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.
- framepin-0.1.0/LICENSE +21 -0
- framepin-0.1.0/PKG-INFO +250 -0
- framepin-0.1.0/README.md +228 -0
- framepin-0.1.0/framepin/__init__.py +28 -0
- framepin-0.1.0/framepin/__main__.py +4 -0
- framepin-0.1.0/framepin/cli.py +212 -0
- framepin-0.1.0/framepin/diff.py +117 -0
- framepin-0.1.0/framepin/hashing.py +62 -0
- framepin-0.1.0/framepin/listfile.py +155 -0
- framepin-0.1.0/framepin/manifest.py +132 -0
- framepin-0.1.0/framepin/repo.py +133 -0
- framepin-0.1.0/framepin/tracking.py +172 -0
- framepin-0.1.0/framepin.egg-info/PKG-INFO +250 -0
- framepin-0.1.0/framepin.egg-info/SOURCES.txt +22 -0
- framepin-0.1.0/framepin.egg-info/dependency_links.txt +1 -0
- framepin-0.1.0/framepin.egg-info/entry_points.txt +2 -0
- framepin-0.1.0/framepin.egg-info/top_level.txt +1 -0
- framepin-0.1.0/pyproject.toml +43 -0
- framepin-0.1.0/setup.cfg +4 -0
- framepin-0.1.0/tests/test_cli.py +107 -0
- framepin-0.1.0/tests/test_diff.py +86 -0
- framepin-0.1.0/tests/test_listfile.py +96 -0
- framepin-0.1.0/tests/test_manifest.py +89 -0
- framepin-0.1.0/tests/test_tracking.py +88 -0
framepin-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 framelock contributors
|
|
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.
|
framepin-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: framepin
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A lockfile for your video/sequence-ML datasets and experiments — reproduce any run without copying a single frame.
|
|
5
|
+
Author: boogy-ro
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/boogy-ro/framepin
|
|
8
|
+
Project-URL: Issues, https://github.com/boogy-ro/framepin/issues
|
|
9
|
+
Keywords: mlops,dataset-versioning,experiment-tracking,reproducibility,video,machine-learning,data-lineage
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
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 :: Only
|
|
16
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
17
|
+
Classifier: Topic :: Software Development :: Version Control
|
|
18
|
+
Requires-Python: >=3.9
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Dynamic: license-file
|
|
22
|
+
|
|
23
|
+
# framepin
|
|
24
|
+
|
|
25
|
+
**A lockfile for your video/sequence-ML datasets and experiments — reproduce any run without copying a single frame.**
|
|
26
|
+
|
|
27
|
+
`framepin` pins the *exact* data a training run saw, links it to the run's
|
|
28
|
+
params/metrics/commit, and tells you — when a metric moves — whether it was the
|
|
29
|
+
**code** or the **data**. Zero dependencies, no server, no data copies. Just
|
|
30
|
+
JSON that commits cleanly to git.
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install git+https://github.com/boogy-ro/framepin # zero dependencies
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
> Status: **v0.1 alpha.** Core (snapshot / diff / track / regress) works and is
|
|
37
|
+
> tested. APIs may shift before 1.0.
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## The problem
|
|
42
|
+
|
|
43
|
+
If you train models on video or sequence data, this has happened to you:
|
|
44
|
+
|
|
45
|
+
- A run from three weeks ago had a better `val_loss`. Which dataset version was
|
|
46
|
+
it? You re-sampled frames and re-labeled twice since. **Gone.**
|
|
47
|
+
- `val_loss` got worse after a change. Was it your code… or did someone swap out
|
|
48
|
+
30% of the clips? **Nobody can say quickly.**
|
|
49
|
+
- `wandb`/`mlflow` track the *run* but treat the dataset as an opaque string.
|
|
50
|
+
`dvc` versions the *data* but is heavy and lives apart from your metrics.
|
|
51
|
+
Neither answers **"is this run reproducible from these exact bytes?"**
|
|
52
|
+
|
|
53
|
+
Video datasets are big, churn constantly, and get reorganized — so a directory
|
|
54
|
+
reshuffle looks like "half my data changed" in a naive diff.
|
|
55
|
+
|
|
56
|
+
## What framepin does
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
import framepin
|
|
60
|
+
|
|
61
|
+
with framepin.track(name="baseline", params={"lr": 3e-4}) as run:
|
|
62
|
+
run.use_dataset("data/clips") # content-hashes + pins this exact version
|
|
63
|
+
model = train(...)
|
|
64
|
+
run.log_metric("val_loss", 0.21) # pinned to that version forever
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Then, three weeks later:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
$ framepin regress <old_run> <new_run> -m val_loss
|
|
71
|
+
regress 02189c0fbd14 -> ab09cc84a35a
|
|
72
|
+
val_loss: 0.21 -> 0.28 (Δ +0.07)
|
|
73
|
+
|
|
74
|
+
⚠ DATA CHANGED between these runs — the dataset version differs.
|
|
75
|
+
A metric move here cannot be attributed to code alone.
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
That last line is the whole point. **framepin separates "the code regressed"
|
|
79
|
+
from "the data changed under you."**
|
|
80
|
+
|
|
81
|
+
## Before / after
|
|
82
|
+
|
|
83
|
+
**Before** — the dataset is a string you hope is still true:
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
run.log({"dataset": "data/clips (v3?)", "val_loss": 0.21})
|
|
87
|
+
# 3 weeks later: what was v3? which frames? was it re-labeled? 🤷
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
**After** — the dataset is a content hash you can reproduce and diff:
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
with framepin.track(name="baseline") as run:
|
|
94
|
+
run.use_dataset("data/clips") # -> version c29aa729c669 (Merkle root of every byte)
|
|
95
|
+
run.log_metric("val_loss", 0.21)
|
|
96
|
+
# later: `framepin show <run>` -> exact version; `framepin diff v1 v2` -> what changed
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## 60-second quickstart
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
cd your-project
|
|
103
|
+
framepin init # creates .framepin/ (commit it to git)
|
|
104
|
+
|
|
105
|
+
framepin snapshot data/clips # -> snapshot c29aa729c669 (N files, no copies)
|
|
106
|
+
# ... change / relabel / resample your data ...
|
|
107
|
+
framepin snapshot data/clips # -> a new version id
|
|
108
|
+
|
|
109
|
+
framepin diff c29aa729c669 <new> # added / removed / modified / MOVED
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Datasets defined by path-list files (train.txt of absolute paths)
|
|
113
|
+
|
|
114
|
+
Many teams don't train on "a directory" — they train on **txt manifests**: one
|
|
115
|
+
absolute path per line (500k clips, several lists concatenated per experiment).
|
|
116
|
+
framepin pins that whole construct — the list files *and* the bytes they
|
|
117
|
+
point at:
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
framepin snapshot --from-list train_urban.txt train_highway.txt
|
|
121
|
+
# -> one version id covering: both txt files + every referenced clip
|
|
122
|
+
# re-encode one clip, or edit one line -> different version id
|
|
123
|
+
# a listed path that no longer exists -> "⚠ recorded as missing"
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Repeated paths across lists are deduped. For 100k+ files, content hashes are
|
|
127
|
+
cached (`.framepin/hashcache.json`) so later snapshots only re-read files
|
|
128
|
+
whose size/mtime changed; `--fast` skips content reads entirely (size+mtime
|
|
129
|
+
fingerprint) for routine checks — take a periodic full snapshot as your anchor
|
|
130
|
+
of record.
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
man = framepin.snapshot_from_lists(["train_urban.txt", "train_highway.txt"])
|
|
134
|
+
with framepin.track(name="exp-8") as run:
|
|
135
|
+
run.use_dataset(man) # pins the exact list+content version
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Track experiments from Python:
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
import framepin
|
|
142
|
+
|
|
143
|
+
with framepin.track(name="exp-7", params={"lr": 3e-4, "aug": "mixup"}) as run:
|
|
144
|
+
run.use_dataset("data/clips")
|
|
145
|
+
for epoch in range(epochs):
|
|
146
|
+
...
|
|
147
|
+
run.log_metric("val_loss", best_val)
|
|
148
|
+
run.log_metric("map50", 0.63)
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Inspect lineage and compare:
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
framepin runs # every run, its metrics, and its data version
|
|
155
|
+
framepin show <run> # full lineage: run -> dataset version -> files
|
|
156
|
+
framepin regress <a> <b> -m map50 # metric delta + "was it code or data?"
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## Why not W&B / MLflow / DVC?
|
|
160
|
+
|
|
161
|
+
| | framepin | W&B / MLflow | DVC |
|
|
162
|
+
|---|---|---|---|
|
|
163
|
+
| Pins run ↔ **exact dataset version** | ✅ first-class | ⚠️ manual string/artifact | ⚠️ separate from metrics |
|
|
164
|
+
| "Code vs data" regression answer | ✅ `regress` | ❌ | ❌ |
|
|
165
|
+
| Detects **moved/renamed** files (not add+remove) | ✅ | — | partial |
|
|
166
|
+
| Copies your data | ❌ never (hashes only) | ⚠️ artifacts | ✅ cache/remote |
|
|
167
|
+
| Server / account required | ❌ | ⚠️ (or self-host) | ❌ |
|
|
168
|
+
| Runtime dependencies | **0** | many | several |
|
|
169
|
+
| Git-friendly plain-JSON store | ✅ | ❌ | ✅ (pointers) |
|
|
170
|
+
|
|
171
|
+
framepin is deliberately small. It is **not** a full experiment platform — it
|
|
172
|
+
does the one thing those tools under-serve: making the *dataset version* a
|
|
173
|
+
first-class, reproducible, diffable part of every run. Use it alongside W&B if
|
|
174
|
+
you like; framepin just owns the data-lineage question.
|
|
175
|
+
|
|
176
|
+
## How it works
|
|
177
|
+
|
|
178
|
+
- **Snapshot** walks a directory, streams a SHA-256 over each file, and records a
|
|
179
|
+
deterministic manifest keyed by a **Merkle root** of `(path, content-hash)`
|
|
180
|
+
pairs. Same bytes → same version id, regardless of walk order or timestamps.
|
|
181
|
+
Your data is never moved or copied — the manifest is a few KB of JSON.
|
|
182
|
+
- **Diff** classifies changes into added / removed / modified / **moved**, so a
|
|
183
|
+
reorg of identical clips reads as moves, not churn.
|
|
184
|
+
- **Track** records a run's params, metrics, git commit, and the dataset
|
|
185
|
+
version(s) it consumed, into `.framepin/runs/<id>.json`.
|
|
186
|
+
- **Regress** compares two runs' metrics *and* their pinned dataset versions.
|
|
187
|
+
|
|
188
|
+
Everything lives under `.framepin/` as plain, sorted JSON. Commit it.
|
|
189
|
+
|
|
190
|
+
## Install from source
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
git clone https://github.com/boogy-ro/framepin
|
|
194
|
+
cd framepin
|
|
195
|
+
pip install -e . # or just add the repo to PYTHONPATH — no deps
|
|
196
|
+
python -m unittest discover -s tests
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Requires Python ≥ 3.9. No third-party packages.
|
|
200
|
+
|
|
201
|
+
See the whole workflow in ~2 seconds (builds a throwaway dataset, simulates a
|
|
202
|
+
re-label + re-encode, then shows the code-vs-data verdict):
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
python3 examples/quickstart_demo.py
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
## Roadmap
|
|
209
|
+
|
|
210
|
+
- `framepin gc` / remote manifest registry for teams
|
|
211
|
+
- CI gate: fail a build when the dataset regresses vs a pinned baseline
|
|
212
|
+
- Optional integrations (export runs to W&B / MLflow)
|
|
213
|
+
- Per-split / per-label manifests for stratified datasets
|
|
214
|
+
|
|
215
|
+
Feedback and issues very welcome — the niche (video/sequence ML data lineage) is
|
|
216
|
+
exactly where this should earn its keep or die. Tell me where it falls short.
|
|
217
|
+
|
|
218
|
+
## FAQ
|
|
219
|
+
|
|
220
|
+
**How do I version an ML dataset without copying it?**
|
|
221
|
+
`framepin snapshot data/clips` content-hashes every file into an immutable
|
|
222
|
+
version id (a Merkle root). Your data never moves; the snapshot is a few KB of
|
|
223
|
+
JSON you commit to git.
|
|
224
|
+
|
|
225
|
+
**How do I know which dataset version an old training run used?**
|
|
226
|
+
If the run was wrapped in `framepin.track(...)` with `run.use_dataset(...)`,
|
|
227
|
+
`framepin show <run>` prints its exact pinned dataset version and file list.
|
|
228
|
+
|
|
229
|
+
**My metric regressed — was it my code or my data?**
|
|
230
|
+
`framepin regress <old_run> <new_run> -m val_loss` compares the two runs'
|
|
231
|
+
metrics *and* their pinned dataset versions, and prints either
|
|
232
|
+
`⚠ DATA CHANGED` or `✓ same dataset version` so you know where to look.
|
|
233
|
+
|
|
234
|
+
**My dataset is a txt file of absolute paths (500k clips), not a directory.**
|
|
235
|
+
`framepin snapshot --from-list train_a.txt train_b.txt` pins the list files
|
|
236
|
+
and every file they reference, deduped across lists. Dead paths are recorded
|
|
237
|
+
as missing. Use `--fast` for size+mtime fingerprints on huge datasets.
|
|
238
|
+
|
|
239
|
+
**Does framepin replace W&B, MLflow, or DVC?**
|
|
240
|
+
No. It runs alongside them and owns one thing they under-serve: the
|
|
241
|
+
dataset-version ↔ run link and the code-vs-data question. No server, no
|
|
242
|
+
account, zero dependencies.
|
|
243
|
+
|
|
244
|
+
**How do I detect that files were renamed/reorganized rather than changed?**
|
|
245
|
+
`framepin diff v1 v2` pairs identical-content files across paths and reports
|
|
246
|
+
them as MOVED instead of added+removed.
|
|
247
|
+
|
|
248
|
+
## License
|
|
249
|
+
|
|
250
|
+
MIT — see [LICENSE](LICENSE).
|
framepin-0.1.0/README.md
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
# framepin
|
|
2
|
+
|
|
3
|
+
**A lockfile for your video/sequence-ML datasets and experiments — reproduce any run without copying a single frame.**
|
|
4
|
+
|
|
5
|
+
`framepin` pins the *exact* data a training run saw, links it to the run's
|
|
6
|
+
params/metrics/commit, and tells you — when a metric moves — whether it was the
|
|
7
|
+
**code** or the **data**. Zero dependencies, no server, no data copies. Just
|
|
8
|
+
JSON that commits cleanly to git.
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
pip install git+https://github.com/boogy-ro/framepin # zero dependencies
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
> Status: **v0.1 alpha.** Core (snapshot / diff / track / regress) works and is
|
|
15
|
+
> tested. APIs may shift before 1.0.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## The problem
|
|
20
|
+
|
|
21
|
+
If you train models on video or sequence data, this has happened to you:
|
|
22
|
+
|
|
23
|
+
- A run from three weeks ago had a better `val_loss`. Which dataset version was
|
|
24
|
+
it? You re-sampled frames and re-labeled twice since. **Gone.**
|
|
25
|
+
- `val_loss` got worse after a change. Was it your code… or did someone swap out
|
|
26
|
+
30% of the clips? **Nobody can say quickly.**
|
|
27
|
+
- `wandb`/`mlflow` track the *run* but treat the dataset as an opaque string.
|
|
28
|
+
`dvc` versions the *data* but is heavy and lives apart from your metrics.
|
|
29
|
+
Neither answers **"is this run reproducible from these exact bytes?"**
|
|
30
|
+
|
|
31
|
+
Video datasets are big, churn constantly, and get reorganized — so a directory
|
|
32
|
+
reshuffle looks like "half my data changed" in a naive diff.
|
|
33
|
+
|
|
34
|
+
## What framepin does
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
import framepin
|
|
38
|
+
|
|
39
|
+
with framepin.track(name="baseline", params={"lr": 3e-4}) as run:
|
|
40
|
+
run.use_dataset("data/clips") # content-hashes + pins this exact version
|
|
41
|
+
model = train(...)
|
|
42
|
+
run.log_metric("val_loss", 0.21) # pinned to that version forever
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Then, three weeks later:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
$ framepin regress <old_run> <new_run> -m val_loss
|
|
49
|
+
regress 02189c0fbd14 -> ab09cc84a35a
|
|
50
|
+
val_loss: 0.21 -> 0.28 (Δ +0.07)
|
|
51
|
+
|
|
52
|
+
⚠ DATA CHANGED between these runs — the dataset version differs.
|
|
53
|
+
A metric move here cannot be attributed to code alone.
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
That last line is the whole point. **framepin separates "the code regressed"
|
|
57
|
+
from "the data changed under you."**
|
|
58
|
+
|
|
59
|
+
## Before / after
|
|
60
|
+
|
|
61
|
+
**Before** — the dataset is a string you hope is still true:
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
run.log({"dataset": "data/clips (v3?)", "val_loss": 0.21})
|
|
65
|
+
# 3 weeks later: what was v3? which frames? was it re-labeled? 🤷
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
**After** — the dataset is a content hash you can reproduce and diff:
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
with framepin.track(name="baseline") as run:
|
|
72
|
+
run.use_dataset("data/clips") # -> version c29aa729c669 (Merkle root of every byte)
|
|
73
|
+
run.log_metric("val_loss", 0.21)
|
|
74
|
+
# later: `framepin show <run>` -> exact version; `framepin diff v1 v2` -> what changed
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## 60-second quickstart
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
cd your-project
|
|
81
|
+
framepin init # creates .framepin/ (commit it to git)
|
|
82
|
+
|
|
83
|
+
framepin snapshot data/clips # -> snapshot c29aa729c669 (N files, no copies)
|
|
84
|
+
# ... change / relabel / resample your data ...
|
|
85
|
+
framepin snapshot data/clips # -> a new version id
|
|
86
|
+
|
|
87
|
+
framepin diff c29aa729c669 <new> # added / removed / modified / MOVED
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Datasets defined by path-list files (train.txt of absolute paths)
|
|
91
|
+
|
|
92
|
+
Many teams don't train on "a directory" — they train on **txt manifests**: one
|
|
93
|
+
absolute path per line (500k clips, several lists concatenated per experiment).
|
|
94
|
+
framepin pins that whole construct — the list files *and* the bytes they
|
|
95
|
+
point at:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
framepin snapshot --from-list train_urban.txt train_highway.txt
|
|
99
|
+
# -> one version id covering: both txt files + every referenced clip
|
|
100
|
+
# re-encode one clip, or edit one line -> different version id
|
|
101
|
+
# a listed path that no longer exists -> "⚠ recorded as missing"
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Repeated paths across lists are deduped. For 100k+ files, content hashes are
|
|
105
|
+
cached (`.framepin/hashcache.json`) so later snapshots only re-read files
|
|
106
|
+
whose size/mtime changed; `--fast` skips content reads entirely (size+mtime
|
|
107
|
+
fingerprint) for routine checks — take a periodic full snapshot as your anchor
|
|
108
|
+
of record.
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
man = framepin.snapshot_from_lists(["train_urban.txt", "train_highway.txt"])
|
|
112
|
+
with framepin.track(name="exp-8") as run:
|
|
113
|
+
run.use_dataset(man) # pins the exact list+content version
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Track experiments from Python:
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
import framepin
|
|
120
|
+
|
|
121
|
+
with framepin.track(name="exp-7", params={"lr": 3e-4, "aug": "mixup"}) as run:
|
|
122
|
+
run.use_dataset("data/clips")
|
|
123
|
+
for epoch in range(epochs):
|
|
124
|
+
...
|
|
125
|
+
run.log_metric("val_loss", best_val)
|
|
126
|
+
run.log_metric("map50", 0.63)
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Inspect lineage and compare:
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
framepin runs # every run, its metrics, and its data version
|
|
133
|
+
framepin show <run> # full lineage: run -> dataset version -> files
|
|
134
|
+
framepin regress <a> <b> -m map50 # metric delta + "was it code or data?"
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Why not W&B / MLflow / DVC?
|
|
138
|
+
|
|
139
|
+
| | framepin | W&B / MLflow | DVC |
|
|
140
|
+
|---|---|---|---|
|
|
141
|
+
| Pins run ↔ **exact dataset version** | ✅ first-class | ⚠️ manual string/artifact | ⚠️ separate from metrics |
|
|
142
|
+
| "Code vs data" regression answer | ✅ `regress` | ❌ | ❌ |
|
|
143
|
+
| Detects **moved/renamed** files (not add+remove) | ✅ | — | partial |
|
|
144
|
+
| Copies your data | ❌ never (hashes only) | ⚠️ artifacts | ✅ cache/remote |
|
|
145
|
+
| Server / account required | ❌ | ⚠️ (or self-host) | ❌ |
|
|
146
|
+
| Runtime dependencies | **0** | many | several |
|
|
147
|
+
| Git-friendly plain-JSON store | ✅ | ❌ | ✅ (pointers) |
|
|
148
|
+
|
|
149
|
+
framepin is deliberately small. It is **not** a full experiment platform — it
|
|
150
|
+
does the one thing those tools under-serve: making the *dataset version* a
|
|
151
|
+
first-class, reproducible, diffable part of every run. Use it alongside W&B if
|
|
152
|
+
you like; framepin just owns the data-lineage question.
|
|
153
|
+
|
|
154
|
+
## How it works
|
|
155
|
+
|
|
156
|
+
- **Snapshot** walks a directory, streams a SHA-256 over each file, and records a
|
|
157
|
+
deterministic manifest keyed by a **Merkle root** of `(path, content-hash)`
|
|
158
|
+
pairs. Same bytes → same version id, regardless of walk order or timestamps.
|
|
159
|
+
Your data is never moved or copied — the manifest is a few KB of JSON.
|
|
160
|
+
- **Diff** classifies changes into added / removed / modified / **moved**, so a
|
|
161
|
+
reorg of identical clips reads as moves, not churn.
|
|
162
|
+
- **Track** records a run's params, metrics, git commit, and the dataset
|
|
163
|
+
version(s) it consumed, into `.framepin/runs/<id>.json`.
|
|
164
|
+
- **Regress** compares two runs' metrics *and* their pinned dataset versions.
|
|
165
|
+
|
|
166
|
+
Everything lives under `.framepin/` as plain, sorted JSON. Commit it.
|
|
167
|
+
|
|
168
|
+
## Install from source
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
git clone https://github.com/boogy-ro/framepin
|
|
172
|
+
cd framepin
|
|
173
|
+
pip install -e . # or just add the repo to PYTHONPATH — no deps
|
|
174
|
+
python -m unittest discover -s tests
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Requires Python ≥ 3.9. No third-party packages.
|
|
178
|
+
|
|
179
|
+
See the whole workflow in ~2 seconds (builds a throwaway dataset, simulates a
|
|
180
|
+
re-label + re-encode, then shows the code-vs-data verdict):
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
python3 examples/quickstart_demo.py
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## Roadmap
|
|
187
|
+
|
|
188
|
+
- `framepin gc` / remote manifest registry for teams
|
|
189
|
+
- CI gate: fail a build when the dataset regresses vs a pinned baseline
|
|
190
|
+
- Optional integrations (export runs to W&B / MLflow)
|
|
191
|
+
- Per-split / per-label manifests for stratified datasets
|
|
192
|
+
|
|
193
|
+
Feedback and issues very welcome — the niche (video/sequence ML data lineage) is
|
|
194
|
+
exactly where this should earn its keep or die. Tell me where it falls short.
|
|
195
|
+
|
|
196
|
+
## FAQ
|
|
197
|
+
|
|
198
|
+
**How do I version an ML dataset without copying it?**
|
|
199
|
+
`framepin snapshot data/clips` content-hashes every file into an immutable
|
|
200
|
+
version id (a Merkle root). Your data never moves; the snapshot is a few KB of
|
|
201
|
+
JSON you commit to git.
|
|
202
|
+
|
|
203
|
+
**How do I know which dataset version an old training run used?**
|
|
204
|
+
If the run was wrapped in `framepin.track(...)` with `run.use_dataset(...)`,
|
|
205
|
+
`framepin show <run>` prints its exact pinned dataset version and file list.
|
|
206
|
+
|
|
207
|
+
**My metric regressed — was it my code or my data?**
|
|
208
|
+
`framepin regress <old_run> <new_run> -m val_loss` compares the two runs'
|
|
209
|
+
metrics *and* their pinned dataset versions, and prints either
|
|
210
|
+
`⚠ DATA CHANGED` or `✓ same dataset version` so you know where to look.
|
|
211
|
+
|
|
212
|
+
**My dataset is a txt file of absolute paths (500k clips), not a directory.**
|
|
213
|
+
`framepin snapshot --from-list train_a.txt train_b.txt` pins the list files
|
|
214
|
+
and every file they reference, deduped across lists. Dead paths are recorded
|
|
215
|
+
as missing. Use `--fast` for size+mtime fingerprints on huge datasets.
|
|
216
|
+
|
|
217
|
+
**Does framepin replace W&B, MLflow, or DVC?**
|
|
218
|
+
No. It runs alongside them and owns one thing they under-serve: the
|
|
219
|
+
dataset-version ↔ run link and the code-vs-data question. No server, no
|
|
220
|
+
account, zero dependencies.
|
|
221
|
+
|
|
222
|
+
**How do I detect that files were renamed/reorganized rather than changed?**
|
|
223
|
+
`framepin diff v1 v2` pairs identical-content files across paths and reports
|
|
224
|
+
them as MOVED instead of added+removed.
|
|
225
|
+
|
|
226
|
+
## License
|
|
227
|
+
|
|
228
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""framepin — a lockfile for your video/sequence-ML datasets and experiments.
|
|
2
|
+
|
|
3
|
+
Serverless, dependency-free, git-friendly versioning + experiment tracking.
|
|
4
|
+
Snapshot a dataset by content (no copies), track a run pinned to that exact
|
|
5
|
+
version, and answer "was it the code or the data?" when a metric moves.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .manifest import Manifest, snapshot
|
|
9
|
+
from .listfile import snapshot_from_lists, HashCache
|
|
10
|
+
from .diff import diff_manifests, DatasetDiff
|
|
11
|
+
from .tracking import track, Run, compare_runs
|
|
12
|
+
from .repo import Repo
|
|
13
|
+
|
|
14
|
+
__version__ = "0.1.0"
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"snapshot_from_lists",
|
|
18
|
+
"HashCache",
|
|
19
|
+
"Manifest",
|
|
20
|
+
"snapshot",
|
|
21
|
+
"diff_manifests",
|
|
22
|
+
"DatasetDiff",
|
|
23
|
+
"track",
|
|
24
|
+
"Run",
|
|
25
|
+
"compare_runs",
|
|
26
|
+
"Repo",
|
|
27
|
+
"__version__",
|
|
28
|
+
]
|