ml-dash 0.5.2__tar.gz → 0.5.6__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.
- {ml_dash-0.5.2 → ml_dash-0.5.6}/PKG-INFO +4 -1
- {ml_dash-0.5.2 → ml_dash-0.5.6}/pyproject.toml +4 -1
- {ml_dash-0.5.2 → ml_dash-0.5.6}/src/ml_dash/experiment.py +10 -10
- {ml_dash-0.5.2 → ml_dash-0.5.6}/src/ml_dash/files.py +281 -26
- {ml_dash-0.5.2 → ml_dash-0.5.6}/src/ml_dash/metric.py +23 -10
- {ml_dash-0.5.2 → ml_dash-0.5.6}/src/ml_dash/storage.py +10 -6
- {ml_dash-0.5.2 → ml_dash-0.5.6}/LICENSE +0 -0
- {ml_dash-0.5.2 → ml_dash-0.5.6}/README.md +0 -0
- {ml_dash-0.5.2 → ml_dash-0.5.6}/src/ml_dash/__init__.py +0 -0
- {ml_dash-0.5.2 → ml_dash-0.5.6}/src/ml_dash/auto_start.py +0 -0
- {ml_dash-0.5.2 → ml_dash-0.5.6}/src/ml_dash/client.py +0 -0
- {ml_dash-0.5.2 → ml_dash-0.5.6}/src/ml_dash/log.py +0 -0
- {ml_dash-0.5.2 → ml_dash-0.5.6}/src/ml_dash/params.py +0 -0
- {ml_dash-0.5.2 → ml_dash-0.5.6}/src/ml_dash/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: ml-dash
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.6
|
|
4
4
|
Summary: ML experiment tracking and data storage
|
|
5
5
|
Keywords: machine-learning,experiment-tracking,mlops,data-storage
|
|
6
6
|
Author: Ge Yang, Tom Tao
|
|
@@ -38,6 +38,9 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
38
38
|
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
39
39
|
Requires-Dist: httpx>=0.27.0
|
|
40
40
|
Requires-Dist: pyjwt>=2.8.0
|
|
41
|
+
Requires-Dist: imageio>=2.31.0
|
|
42
|
+
Requires-Dist: imageio-ffmpeg>=0.4.9
|
|
43
|
+
Requires-Dist: scikit-image>=0.21.0
|
|
41
44
|
Requires-Dist: pytest>=8.0.0 ; extra == 'dev'
|
|
42
45
|
Requires-Dist: pytest-asyncio>=0.23.0 ; extra == 'dev'
|
|
43
46
|
Requires-Dist: sphinx>=7.2.0 ; extra == 'dev'
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "ml-dash"
|
|
3
|
-
version = "0.5.
|
|
3
|
+
version = "0.5.6"
|
|
4
4
|
description = "ML experiment tracking and data storage"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.9"
|
|
@@ -26,6 +26,9 @@ classifiers = [
|
|
|
26
26
|
dependencies = [
|
|
27
27
|
"httpx>=0.27.0",
|
|
28
28
|
"pyjwt>=2.8.0",
|
|
29
|
+
"imageio>=2.31.0",
|
|
30
|
+
"imageio-ffmpeg>=0.4.9",
|
|
31
|
+
"scikit-image>=0.21.0",
|
|
29
32
|
]
|
|
30
33
|
|
|
31
34
|
[project.optional-dependencies]
|
|
@@ -460,7 +460,7 @@ class Experiment:
|
|
|
460
460
|
timestamp=log_entry["timestamp"]
|
|
461
461
|
)
|
|
462
462
|
|
|
463
|
-
def
|
|
463
|
+
def files(self, **kwargs) -> FileBuilder:
|
|
464
464
|
"""
|
|
465
465
|
Get a FileBuilder for fluent file operations.
|
|
466
466
|
|
|
@@ -472,17 +472,17 @@ class Experiment:
|
|
|
472
472
|
|
|
473
473
|
Examples:
|
|
474
474
|
# Upload file
|
|
475
|
-
experiment.
|
|
475
|
+
experiment.files(file_path="./model.pt", prefix="/models").save()
|
|
476
476
|
|
|
477
477
|
# List files
|
|
478
|
-
files = experiment.
|
|
479
|
-
files = experiment.
|
|
478
|
+
files = experiment.files().list()
|
|
479
|
+
files = experiment.files(prefix="/models").list()
|
|
480
480
|
|
|
481
481
|
# Download file
|
|
482
|
-
experiment.
|
|
482
|
+
experiment.files(file_id="123").download()
|
|
483
483
|
|
|
484
484
|
# Delete file
|
|
485
|
-
experiment.
|
|
485
|
+
experiment.files(file_id="123").delete()
|
|
486
486
|
"""
|
|
487
487
|
if not self._is_open:
|
|
488
488
|
raise RuntimeError("Experiment not open. Use experiment.open() or context manager.")
|
|
@@ -791,7 +791,7 @@ class Experiment:
|
|
|
791
791
|
|
|
792
792
|
def _append_to_metric(
|
|
793
793
|
self,
|
|
794
|
-
name: str,
|
|
794
|
+
name: Optional[str],
|
|
795
795
|
data: Dict[str, Any],
|
|
796
796
|
description: Optional[str],
|
|
797
797
|
tags: Optional[List[str]],
|
|
@@ -801,7 +801,7 @@ class Experiment:
|
|
|
801
801
|
Internal method to append a single data point to a metric.
|
|
802
802
|
|
|
803
803
|
Args:
|
|
804
|
-
name: Metric name
|
|
804
|
+
name: Metric name (can be None for unnamed metrics)
|
|
805
805
|
data: Data point (flexible schema)
|
|
806
806
|
description: Optional metric description
|
|
807
807
|
tags: Optional tags
|
|
@@ -839,7 +839,7 @@ class Experiment:
|
|
|
839
839
|
|
|
840
840
|
def _append_batch_to_metric(
|
|
841
841
|
self,
|
|
842
|
-
name: str,
|
|
842
|
+
name: Optional[str],
|
|
843
843
|
data_points: List[Dict[str, Any]],
|
|
844
844
|
description: Optional[str],
|
|
845
845
|
tags: Optional[List[str]],
|
|
@@ -849,7 +849,7 @@ class Experiment:
|
|
|
849
849
|
Internal method to append multiple data points to a metric.
|
|
850
850
|
|
|
851
851
|
Args:
|
|
852
|
-
name: Metric name
|
|
852
|
+
name: Metric name (can be None for unnamed metrics)
|
|
853
853
|
data_points: List of data points
|
|
854
854
|
description: Optional metric description
|
|
855
855
|
tags: Optional tags
|
|
@@ -6,7 +6,7 @@ Provides fluent API for file upload, download, list, and delete operations.
|
|
|
6
6
|
|
|
7
7
|
import hashlib
|
|
8
8
|
import mimetypes
|
|
9
|
-
from typing import Dict, Any, List, Optional, TYPE_CHECKING
|
|
9
|
+
from typing import Dict, Any, List, Optional, Union, TYPE_CHECKING
|
|
10
10
|
from pathlib import Path
|
|
11
11
|
|
|
12
12
|
if TYPE_CHECKING:
|
|
@@ -19,18 +19,18 @@ class FileBuilder:
|
|
|
19
19
|
|
|
20
20
|
Usage:
|
|
21
21
|
# Upload file
|
|
22
|
-
experiment.
|
|
22
|
+
experiment.files(file_path="./model.pt", prefix="/models").save()
|
|
23
23
|
|
|
24
24
|
# List files
|
|
25
|
-
files = experiment.
|
|
26
|
-
files = experiment.
|
|
25
|
+
files = experiment.files().list()
|
|
26
|
+
files = experiment.files(prefix="/models").list()
|
|
27
27
|
|
|
28
28
|
# Download file
|
|
29
|
-
experiment.
|
|
30
|
-
experiment.
|
|
29
|
+
experiment.files(file_id="123").download()
|
|
30
|
+
experiment.files(file_id="123", dest_path="./model.pt").download()
|
|
31
31
|
|
|
32
32
|
# Delete file
|
|
33
|
-
experiment.
|
|
33
|
+
experiment.files(file_id="123").delete()
|
|
34
34
|
"""
|
|
35
35
|
|
|
36
36
|
def __init__(self, experiment: 'Experiment', **kwargs):
|
|
@@ -72,7 +72,7 @@ class FileBuilder:
|
|
|
72
72
|
ValueError: If file size exceeds 5GB limit
|
|
73
73
|
|
|
74
74
|
Examples:
|
|
75
|
-
result = experiment.
|
|
75
|
+
result = experiment.files(file_path="./model.pt", prefix="/models").save()
|
|
76
76
|
# Returns: {"id": "123", "path": "/models", "filename": "model.pt", ...}
|
|
77
77
|
"""
|
|
78
78
|
if not self._experiment._is_open:
|
|
@@ -130,9 +130,9 @@ class FileBuilder:
|
|
|
130
130
|
RuntimeError: If experiment is not open
|
|
131
131
|
|
|
132
132
|
Examples:
|
|
133
|
-
files = experiment.
|
|
134
|
-
files = experiment.
|
|
135
|
-
files = experiment.
|
|
133
|
+
files = experiment.files().list() # All files
|
|
134
|
+
files = experiment.files(prefix="/models").list() # Filter by prefix
|
|
135
|
+
files = experiment.files(tags=["checkpoint"]).list() # Filter by tags
|
|
136
136
|
"""
|
|
137
137
|
if not self._experiment._is_open:
|
|
138
138
|
raise RuntimeError("Experiment not open. Use experiment.open() or context manager.")
|
|
@@ -158,10 +158,10 @@ class FileBuilder:
|
|
|
158
158
|
|
|
159
159
|
Examples:
|
|
160
160
|
# Download to current directory with original filename
|
|
161
|
-
path = experiment.
|
|
161
|
+
path = experiment.files(file_id="123").download()
|
|
162
162
|
|
|
163
163
|
# Download to custom path
|
|
164
|
-
path = experiment.
|
|
164
|
+
path = experiment.files(file_id="123", dest_path="./model.pt").download()
|
|
165
165
|
"""
|
|
166
166
|
if not self._experiment._is_open:
|
|
167
167
|
raise RuntimeError("Experiment not open. Use experiment.open() or context manager.")
|
|
@@ -186,7 +186,7 @@ class FileBuilder:
|
|
|
186
186
|
ValueError: If file_id not provided
|
|
187
187
|
|
|
188
188
|
Examples:
|
|
189
|
-
result = experiment.
|
|
189
|
+
result = experiment.files(file_id="123").delete()
|
|
190
190
|
"""
|
|
191
191
|
if not self._experiment._is_open:
|
|
192
192
|
raise RuntimeError("Experiment not open. Use experiment.open() or context manager.")
|
|
@@ -211,7 +211,7 @@ class FileBuilder:
|
|
|
211
211
|
ValueError: If file_id not provided
|
|
212
212
|
|
|
213
213
|
Examples:
|
|
214
|
-
result = experiment.
|
|
214
|
+
result = experiment.files(
|
|
215
215
|
file_id="123",
|
|
216
216
|
description="Updated description",
|
|
217
217
|
tags=["new", "tags"],
|
|
@@ -251,7 +251,7 @@ class FileBuilder:
|
|
|
251
251
|
|
|
252
252
|
Examples:
|
|
253
253
|
config = {"model": "resnet50", "lr": 0.001}
|
|
254
|
-
result = experiment.
|
|
254
|
+
result = experiment.files(prefix="/configs").save_json(config, "config.json")
|
|
255
255
|
"""
|
|
256
256
|
import json
|
|
257
257
|
import tempfile
|
|
@@ -263,11 +263,12 @@ class FileBuilder:
|
|
|
263
263
|
if self._experiment._write_protected:
|
|
264
264
|
raise RuntimeError("Experiment is write-protected and cannot be modified.")
|
|
265
265
|
|
|
266
|
-
# Create temporary file
|
|
267
|
-
|
|
266
|
+
# Create temporary file with desired filename
|
|
267
|
+
temp_dir = tempfile.mkdtemp()
|
|
268
|
+
temp_path = os.path.join(temp_dir, file_name)
|
|
268
269
|
try:
|
|
269
270
|
# Write JSON content to temp file
|
|
270
|
-
with
|
|
271
|
+
with open(temp_path, 'w') as f:
|
|
271
272
|
json.dump(content, f, indent=2)
|
|
272
273
|
|
|
273
274
|
# Save using existing save() method
|
|
@@ -282,9 +283,10 @@ class FileBuilder:
|
|
|
282
283
|
|
|
283
284
|
return result
|
|
284
285
|
finally:
|
|
285
|
-
# Clean up temp file
|
|
286
|
+
# Clean up temp file and directory
|
|
286
287
|
try:
|
|
287
288
|
os.unlink(temp_path)
|
|
289
|
+
os.rmdir(temp_dir)
|
|
288
290
|
except Exception:
|
|
289
291
|
pass
|
|
290
292
|
|
|
@@ -307,10 +309,10 @@ class FileBuilder:
|
|
|
307
309
|
Examples:
|
|
308
310
|
import torch
|
|
309
311
|
model = torch.nn.Linear(10, 5)
|
|
310
|
-
result = experiment.
|
|
312
|
+
result = experiment.files(prefix="/models").save_torch(model, "model.pt")
|
|
311
313
|
|
|
312
314
|
# Or save state dict
|
|
313
|
-
result = experiment.
|
|
315
|
+
result = experiment.files(prefix="/models").save_torch(model.state_dict(), "model.pth")
|
|
314
316
|
"""
|
|
315
317
|
import tempfile
|
|
316
318
|
import os
|
|
@@ -326,9 +328,9 @@ class FileBuilder:
|
|
|
326
328
|
if self._experiment._write_protected:
|
|
327
329
|
raise RuntimeError("Experiment is write-protected and cannot be modified.")
|
|
328
330
|
|
|
329
|
-
# Create temporary file
|
|
330
|
-
|
|
331
|
-
os.
|
|
331
|
+
# Create temporary file with desired filename
|
|
332
|
+
temp_dir = tempfile.mkdtemp()
|
|
333
|
+
temp_path = os.path.join(temp_dir, file_name)
|
|
332
334
|
|
|
333
335
|
try:
|
|
334
336
|
# Save model to temp file
|
|
@@ -346,9 +348,262 @@ class FileBuilder:
|
|
|
346
348
|
|
|
347
349
|
return result
|
|
348
350
|
finally:
|
|
349
|
-
# Clean up temp file
|
|
351
|
+
# Clean up temp file and directory
|
|
350
352
|
try:
|
|
351
353
|
os.unlink(temp_path)
|
|
354
|
+
os.rmdir(temp_dir)
|
|
355
|
+
except Exception:
|
|
356
|
+
pass
|
|
357
|
+
|
|
358
|
+
def save_pkl(self, content: Any, file_name: str) -> Dict[str, Any]:
|
|
359
|
+
"""
|
|
360
|
+
Save Python object to a pickle file.
|
|
361
|
+
|
|
362
|
+
Args:
|
|
363
|
+
content: Python object to pickle (must be pickle-serializable)
|
|
364
|
+
file_name: Name of the file to create (should end with .pkl or .pickle)
|
|
365
|
+
|
|
366
|
+
Returns:
|
|
367
|
+
File metadata dict with id, path, filename, checksum, etc.
|
|
368
|
+
|
|
369
|
+
Raises:
|
|
370
|
+
RuntimeError: If experiment is not open or write-protected
|
|
371
|
+
ValueError: If content cannot be pickled
|
|
372
|
+
|
|
373
|
+
Examples:
|
|
374
|
+
data = {"model": "resnet50", "weights": np.array([1, 2, 3])}
|
|
375
|
+
result = experiment.files(prefix="/data").save_pkl(data, "data.pkl")
|
|
376
|
+
|
|
377
|
+
# Or save any Python object
|
|
378
|
+
result = experiment.files(prefix="/models").save_pkl(trained_model, "model.pickle")
|
|
379
|
+
"""
|
|
380
|
+
import pickle
|
|
381
|
+
import tempfile
|
|
382
|
+
import os
|
|
383
|
+
|
|
384
|
+
if not self._experiment._is_open:
|
|
385
|
+
raise RuntimeError("Experiment not open. Use experiment.run.start() or context manager.")
|
|
386
|
+
|
|
387
|
+
if self._experiment._write_protected:
|
|
388
|
+
raise RuntimeError("Experiment is write-protected and cannot be modified.")
|
|
389
|
+
|
|
390
|
+
# Create temporary file with desired filename
|
|
391
|
+
temp_dir = tempfile.mkdtemp()
|
|
392
|
+
temp_path = os.path.join(temp_dir, file_name)
|
|
393
|
+
try:
|
|
394
|
+
# Write pickled content to temp file
|
|
395
|
+
with open(temp_path, 'wb') as f:
|
|
396
|
+
pickle.dump(content, f)
|
|
397
|
+
|
|
398
|
+
# Save using existing save() method
|
|
399
|
+
original_file_path = self._file_path
|
|
400
|
+
self._file_path = temp_path
|
|
401
|
+
|
|
402
|
+
# Upload and get result
|
|
403
|
+
result = self.save()
|
|
404
|
+
|
|
405
|
+
# Restore original file_path
|
|
406
|
+
self._file_path = original_file_path
|
|
407
|
+
|
|
408
|
+
return result
|
|
409
|
+
finally:
|
|
410
|
+
# Clean up temp file and directory
|
|
411
|
+
try:
|
|
412
|
+
os.unlink(temp_path)
|
|
413
|
+
os.rmdir(temp_dir)
|
|
414
|
+
except Exception:
|
|
415
|
+
pass
|
|
416
|
+
|
|
417
|
+
def save_fig(self, fig: Optional[Any] = None, file_name: str = "figure.png", **kwargs) -> Dict[str, Any]:
|
|
418
|
+
"""
|
|
419
|
+
Save matplotlib figure to a file.
|
|
420
|
+
|
|
421
|
+
Args:
|
|
422
|
+
fig: Matplotlib figure object. If None, uses plt.gcf() (current figure)
|
|
423
|
+
file_name: Name of file to create (extension determines format: .png, .pdf, .svg, .jpg)
|
|
424
|
+
**kwargs: Additional arguments passed to fig.savefig():
|
|
425
|
+
- dpi: Resolution (int or 'figure')
|
|
426
|
+
- transparent: Make background transparent (bool)
|
|
427
|
+
- bbox_inches: 'tight' to auto-crop (str or Bbox)
|
|
428
|
+
- quality: JPEG quality 0-100 (int)
|
|
429
|
+
- format: Override format detection (str)
|
|
430
|
+
|
|
431
|
+
Returns:
|
|
432
|
+
File metadata dict with id, path, filename, checksum, etc.
|
|
433
|
+
|
|
434
|
+
Raises:
|
|
435
|
+
RuntimeError: If experiment not open or write-protected
|
|
436
|
+
ImportError: If matplotlib not installed
|
|
437
|
+
|
|
438
|
+
Examples:
|
|
439
|
+
import matplotlib.pyplot as plt
|
|
440
|
+
|
|
441
|
+
# Use current figure
|
|
442
|
+
plt.plot([1, 2, 3], [1, 4, 9])
|
|
443
|
+
result = experiment.files(prefix="/plots").save_fig(file_name="plot.png")
|
|
444
|
+
|
|
445
|
+
# Specify figure explicitly
|
|
446
|
+
fig, ax = plt.subplots()
|
|
447
|
+
ax.plot([1, 2, 3])
|
|
448
|
+
result = experiment.files(prefix="/plots").save_fig(fig=fig, file_name="plot.pdf", dpi=150)
|
|
449
|
+
"""
|
|
450
|
+
import tempfile
|
|
451
|
+
import os
|
|
452
|
+
|
|
453
|
+
try:
|
|
454
|
+
import matplotlib.pyplot as plt
|
|
455
|
+
except ImportError:
|
|
456
|
+
raise ImportError("Matplotlib is not installed. Install it with: pip install matplotlib")
|
|
457
|
+
|
|
458
|
+
if not self._experiment._is_open:
|
|
459
|
+
raise RuntimeError("Experiment not open. Use experiment.run.start() or context manager.")
|
|
460
|
+
|
|
461
|
+
if self._experiment._write_protected:
|
|
462
|
+
raise RuntimeError("Experiment is write-protected and cannot be modified.")
|
|
463
|
+
|
|
464
|
+
# Get figure
|
|
465
|
+
if fig is None:
|
|
466
|
+
fig = plt.gcf()
|
|
467
|
+
|
|
468
|
+
# Create temporary file with desired filename
|
|
469
|
+
temp_dir = tempfile.mkdtemp()
|
|
470
|
+
temp_path = os.path.join(temp_dir, file_name)
|
|
471
|
+
|
|
472
|
+
try:
|
|
473
|
+
# Save figure to temp file
|
|
474
|
+
fig.savefig(temp_path, **kwargs)
|
|
475
|
+
|
|
476
|
+
# Close figure to prevent memory leaks
|
|
477
|
+
plt.close(fig)
|
|
478
|
+
|
|
479
|
+
# Save using existing save() method
|
|
480
|
+
original_file_path = self._file_path
|
|
481
|
+
self._file_path = temp_path
|
|
482
|
+
|
|
483
|
+
# Upload and get result
|
|
484
|
+
result = self.save()
|
|
485
|
+
|
|
486
|
+
# Restore original file_path
|
|
487
|
+
self._file_path = original_file_path
|
|
488
|
+
|
|
489
|
+
return result
|
|
490
|
+
finally:
|
|
491
|
+
# Clean up temp file and directory
|
|
492
|
+
try:
|
|
493
|
+
os.unlink(temp_path)
|
|
494
|
+
os.rmdir(temp_dir)
|
|
495
|
+
except Exception:
|
|
496
|
+
pass
|
|
497
|
+
|
|
498
|
+
def save_video(
|
|
499
|
+
self,
|
|
500
|
+
frame_stack: Union[List, Any],
|
|
501
|
+
file_name: str,
|
|
502
|
+
fps: int = 20,
|
|
503
|
+
**imageio_kwargs
|
|
504
|
+
) -> Dict[str, Any]:
|
|
505
|
+
"""
|
|
506
|
+
Save video frame stack to a file.
|
|
507
|
+
|
|
508
|
+
Args:
|
|
509
|
+
frame_stack: List of numpy arrays or stacked array (shape: [N, H, W] or [N, H, W, C])
|
|
510
|
+
file_name: Name of file to create (extension determines format: .mp4, .gif, .avi, .webm)
|
|
511
|
+
fps: Frames per second (default: 20)
|
|
512
|
+
**imageio_kwargs: Additional arguments passed to imageio.v3.imwrite():
|
|
513
|
+
- codec: Video codec (e.g., 'libx264', 'h264')
|
|
514
|
+
- quality: Quality level (int, higher is better)
|
|
515
|
+
- pixelformat: Pixel format (e.g., 'yuv420p')
|
|
516
|
+
- macro_block_size: Macro block size for encoding
|
|
517
|
+
|
|
518
|
+
Returns:
|
|
519
|
+
File metadata dict with id, path, filename, checksum, etc.
|
|
520
|
+
|
|
521
|
+
Raises:
|
|
522
|
+
RuntimeError: If experiment not open or write-protected
|
|
523
|
+
ImportError: If imageio or scikit-image not installed
|
|
524
|
+
ValueError: If frame_stack is empty or invalid format
|
|
525
|
+
|
|
526
|
+
Examples:
|
|
527
|
+
import numpy as np
|
|
528
|
+
|
|
529
|
+
# Grayscale frames (float values 0-1)
|
|
530
|
+
frames = [np.random.rand(480, 640) for _ in range(30)]
|
|
531
|
+
result = experiment.files(prefix="/videos").save_video(frames, "output.mp4")
|
|
532
|
+
|
|
533
|
+
# RGB frames with custom FPS
|
|
534
|
+
frames = [np.random.rand(480, 640, 3) for _ in range(60)]
|
|
535
|
+
result = experiment.files(prefix="/videos").save_video(frames, "output.mp4", fps=30)
|
|
536
|
+
|
|
537
|
+
# Save as GIF
|
|
538
|
+
frames = [np.random.rand(200, 200) for _ in range(20)]
|
|
539
|
+
result = experiment.files(prefix="/videos").save_video(frames, "animation.gif")
|
|
540
|
+
|
|
541
|
+
# With custom codec and quality
|
|
542
|
+
result = experiment.files(prefix="/videos").save_video(
|
|
543
|
+
frames, "output.mp4", fps=30, codec='libx264', quality=8
|
|
544
|
+
)
|
|
545
|
+
"""
|
|
546
|
+
import tempfile
|
|
547
|
+
import os
|
|
548
|
+
|
|
549
|
+
try:
|
|
550
|
+
import imageio.v3 as iio
|
|
551
|
+
except ImportError:
|
|
552
|
+
raise ImportError("imageio is not installed. Install it with: pip install imageio imageio-ffmpeg")
|
|
553
|
+
|
|
554
|
+
try:
|
|
555
|
+
from skimage import img_as_ubyte
|
|
556
|
+
except ImportError:
|
|
557
|
+
raise ImportError("scikit-image is not installed. Install it with: pip install scikit-image")
|
|
558
|
+
|
|
559
|
+
if not self._experiment._is_open:
|
|
560
|
+
raise RuntimeError("Experiment not open. Use experiment.run.start() or context manager.")
|
|
561
|
+
|
|
562
|
+
if self._experiment._write_protected:
|
|
563
|
+
raise RuntimeError("Experiment is write-protected and cannot be modified.")
|
|
564
|
+
|
|
565
|
+
# Validate frame_stack
|
|
566
|
+
try:
|
|
567
|
+
# Handle both list and numpy array
|
|
568
|
+
if len(frame_stack) == 0:
|
|
569
|
+
raise ValueError("frame_stack is empty")
|
|
570
|
+
except TypeError:
|
|
571
|
+
raise ValueError("frame_stack must be a list or numpy array")
|
|
572
|
+
|
|
573
|
+
# Create temporary file with desired filename
|
|
574
|
+
temp_dir = tempfile.mkdtemp()
|
|
575
|
+
temp_path = os.path.join(temp_dir, file_name)
|
|
576
|
+
|
|
577
|
+
try:
|
|
578
|
+
# Convert frames to uint8 format (handles float32/64, grayscale, RGB, etc.)
|
|
579
|
+
# img_as_ubyte automatically scales [0.0, 1.0] floats to [0, 255] uint8
|
|
580
|
+
frames_ubyte = img_as_ubyte(frame_stack)
|
|
581
|
+
|
|
582
|
+
# Encode video to temp file
|
|
583
|
+
try:
|
|
584
|
+
iio.imwrite(temp_path, frames_ubyte, fps=fps, **imageio_kwargs)
|
|
585
|
+
except iio.core.NeedDownloadError:
|
|
586
|
+
# Auto-download FFmpeg if not available
|
|
587
|
+
import imageio.plugins.ffmpeg
|
|
588
|
+
imageio.plugins.ffmpeg.download()
|
|
589
|
+
iio.imwrite(temp_path, frames_ubyte, fps=fps, **imageio_kwargs)
|
|
590
|
+
|
|
591
|
+
# Save using existing save() method
|
|
592
|
+
original_file_path = self._file_path
|
|
593
|
+
self._file_path = temp_path
|
|
594
|
+
|
|
595
|
+
# Upload and get result
|
|
596
|
+
result = self.save()
|
|
597
|
+
|
|
598
|
+
# Restore original file_path
|
|
599
|
+
self._file_path = original_file_path
|
|
600
|
+
|
|
601
|
+
return result
|
|
602
|
+
finally:
|
|
603
|
+
# Clean up temp file and directory
|
|
604
|
+
try:
|
|
605
|
+
os.unlink(temp_path)
|
|
606
|
+
os.rmdir(temp_dir)
|
|
352
607
|
except Exception:
|
|
353
608
|
pass
|
|
354
609
|
|
|
@@ -15,16 +15,20 @@ class MetricsManager:
|
|
|
15
15
|
"""
|
|
16
16
|
Manager for metric operations that supports both named and unnamed usage.
|
|
17
17
|
|
|
18
|
-
Supports
|
|
19
|
-
1. Named: experiment.metrics("loss").append(value=0.5, step=1)
|
|
20
|
-
2.
|
|
18
|
+
Supports three usage patterns:
|
|
19
|
+
1. Named via call: experiment.metrics("loss").append(value=0.5, step=1)
|
|
20
|
+
2. Named via argument: experiment.metrics.append(name="loss", value=0.5, step=1)
|
|
21
|
+
3. Unnamed: experiment.metrics.append(value=0.5, step=1) # name=None
|
|
21
22
|
|
|
22
23
|
Usage:
|
|
23
|
-
# With explicit metric name
|
|
24
|
+
# With explicit metric name (via call)
|
|
24
25
|
experiment.metrics("train_loss").append(value=0.5, step=100)
|
|
25
26
|
|
|
26
|
-
#
|
|
27
|
+
# With explicit metric name (via argument)
|
|
27
28
|
experiment.metrics.append(name="train_loss", value=0.5, step=100)
|
|
29
|
+
|
|
30
|
+
# Without name (uses None as metric name)
|
|
31
|
+
experiment.metrics.append(value=0.5, step=100)
|
|
28
32
|
"""
|
|
29
33
|
|
|
30
34
|
def __init__(self, experiment: 'Experiment'):
|
|
@@ -55,12 +59,12 @@ class MetricsManager:
|
|
|
55
59
|
"""
|
|
56
60
|
return MetricBuilder(self._experiment, name, description, tags, metadata)
|
|
57
61
|
|
|
58
|
-
def append(self, name: str, data: Optional[Dict[str, Any]] = None, **kwargs) -> Dict[str, Any]:
|
|
62
|
+
def append(self, name: Optional[str] = None, data: Optional[Dict[str, Any]] = None, **kwargs) -> Dict[str, Any]:
|
|
59
63
|
"""
|
|
60
|
-
Append a data point to a metric (name
|
|
64
|
+
Append a data point to a metric (name can be optional).
|
|
61
65
|
|
|
62
66
|
Args:
|
|
63
|
-
name: Metric name
|
|
67
|
+
name: Metric name (optional, can be None for unnamed metrics)
|
|
64
68
|
data: Data dict (alternative to kwargs)
|
|
65
69
|
**kwargs: Data as keyword arguments
|
|
66
70
|
|
|
@@ -69,13 +73,14 @@ class MetricsManager:
|
|
|
69
73
|
|
|
70
74
|
Examples:
|
|
71
75
|
experiment.metrics.append(name="loss", value=0.5, step=1)
|
|
76
|
+
experiment.metrics.append(value=0.5, step=1) # name=None
|
|
72
77
|
experiment.metrics.append(name="loss", data={"value": 0.5, "step": 1})
|
|
73
78
|
"""
|
|
74
79
|
if data is None:
|
|
75
80
|
data = kwargs
|
|
76
81
|
return self._experiment._append_to_metric(name, data, None, None, None)
|
|
77
82
|
|
|
78
|
-
def append_batch(self, name: str, data_points: List[Dict[str, Any]],
|
|
83
|
+
def append_batch(self, name: Optional[str] = None, data_points: Optional[List[Dict[str, Any]]] = None,
|
|
79
84
|
description: Optional[str] = None,
|
|
80
85
|
tags: Optional[List[str]] = None,
|
|
81
86
|
metadata: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
@@ -83,7 +88,7 @@ class MetricsManager:
|
|
|
83
88
|
Append multiple data points to a metric.
|
|
84
89
|
|
|
85
90
|
Args:
|
|
86
|
-
name: Metric name
|
|
91
|
+
name: Metric name (optional, can be None for unnamed metrics)
|
|
87
92
|
data_points: List of data point dicts
|
|
88
93
|
description: Optional metric description
|
|
89
94
|
tags: Optional tags for categorization
|
|
@@ -100,7 +105,15 @@ class MetricsManager:
|
|
|
100
105
|
{"value": 0.4, "step": 2}
|
|
101
106
|
]
|
|
102
107
|
)
|
|
108
|
+
experiment.metrics.append_batch(
|
|
109
|
+
data_points=[
|
|
110
|
+
{"value": 0.5, "step": 1},
|
|
111
|
+
{"value": 0.4, "step": 2}
|
|
112
|
+
]
|
|
113
|
+
) # name=None
|
|
103
114
|
"""
|
|
115
|
+
if data_points is None:
|
|
116
|
+
data_points = []
|
|
104
117
|
return self._experiment._append_batch_to_metric(name, data_points, description, tags, metadata)
|
|
105
118
|
|
|
106
119
|
|
|
@@ -636,7 +636,7 @@ class LocalStorage:
|
|
|
636
636
|
self,
|
|
637
637
|
project: str,
|
|
638
638
|
experiment: str,
|
|
639
|
-
metric_name: str,
|
|
639
|
+
metric_name: Optional[str],
|
|
640
640
|
data: Dict[str, Any],
|
|
641
641
|
description: Optional[str] = None,
|
|
642
642
|
tags: Optional[List[str]] = None,
|
|
@@ -653,7 +653,7 @@ class LocalStorage:
|
|
|
653
653
|
Args:
|
|
654
654
|
project: Project name
|
|
655
655
|
experiment: Experiment name
|
|
656
|
-
metric_name: Metric name
|
|
656
|
+
metric_name: Metric name (None for unnamed metrics)
|
|
657
657
|
data: Data point (flexible schema)
|
|
658
658
|
description: Optional metric description
|
|
659
659
|
tags: Optional tags
|
|
@@ -666,7 +666,9 @@ class LocalStorage:
|
|
|
666
666
|
metrics_dir = experiment_dir / "metrics"
|
|
667
667
|
metrics_dir.mkdir(parents=True, exist_ok=True)
|
|
668
668
|
|
|
669
|
-
|
|
669
|
+
# Convert None to string for directory name
|
|
670
|
+
dir_name = str(metric_name) if metric_name is not None else "None"
|
|
671
|
+
metric_dir = metrics_dir / dir_name
|
|
670
672
|
metric_dir.mkdir(exist_ok=True)
|
|
671
673
|
|
|
672
674
|
data_file = metric_dir / "data.jsonl"
|
|
@@ -720,7 +722,7 @@ class LocalStorage:
|
|
|
720
722
|
self,
|
|
721
723
|
project: str,
|
|
722
724
|
experiment: str,
|
|
723
|
-
metric_name: str,
|
|
725
|
+
metric_name: Optional[str],
|
|
724
726
|
data_points: List[Dict[str, Any]],
|
|
725
727
|
description: Optional[str] = None,
|
|
726
728
|
tags: Optional[List[str]] = None,
|
|
@@ -732,7 +734,7 @@ class LocalStorage:
|
|
|
732
734
|
Args:
|
|
733
735
|
project: Project name
|
|
734
736
|
experiment: Experiment name
|
|
735
|
-
metric_name: Metric name
|
|
737
|
+
metric_name: Metric name (None for unnamed metrics)
|
|
736
738
|
data_points: List of data points
|
|
737
739
|
description: Optional metric description
|
|
738
740
|
tags: Optional tags
|
|
@@ -745,7 +747,9 @@ class LocalStorage:
|
|
|
745
747
|
metrics_dir = experiment_dir / "metrics"
|
|
746
748
|
metrics_dir.mkdir(parents=True, exist_ok=True)
|
|
747
749
|
|
|
748
|
-
|
|
750
|
+
# Convert None to string for directory name
|
|
751
|
+
dir_name = str(metric_name) if metric_name is not None else "None"
|
|
752
|
+
metric_dir = metrics_dir / dir_name
|
|
749
753
|
metric_dir.mkdir(exist_ok=True)
|
|
750
754
|
|
|
751
755
|
data_file = metric_dir / "data.jsonl"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|