mozo 0.1.0__py3-none-any.whl → 0.2.0__py3-none-any.whl
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.
- mozo/__init__.py +50 -5
- mozo/__main__.py +24 -0
- mozo/adapters/__init__.py +0 -0
- mozo/adapters/depth_anything.py +75 -0
- mozo/adapters/detectron2.py +128 -0
- mozo/adapters/qwen2_5_vl.py +170 -0
- mozo/cli.py +47 -0
- mozo/factory.py +150 -0
- mozo/manager.py +294 -0
- mozo/registry.py +235 -0
- mozo/server.py +294 -0
- mozo-0.2.0.dist-info/METADATA +343 -0
- mozo-0.2.0.dist-info/RECORD +17 -0
- mozo-0.2.0.dist-info/entry_points.txt +2 -0
- {mozo-0.1.0.dist-info → mozo-0.2.0.dist-info}/licenses/LICENSE +2 -2
- mozo-0.1.0.dist-info/METADATA +0 -58
- mozo-0.1.0.dist-info/RECORD +0 -6
- {mozo-0.1.0.dist-info → mozo-0.2.0.dist-info}/WHEEL +0 -0
- {mozo-0.1.0.dist-info → mozo-0.2.0.dist-info}/top_level.txt +0 -0
mozo/manager.py
ADDED
@@ -0,0 +1,294 @@
|
|
1
|
+
"""
|
2
|
+
Model Manager for Mozo
|
3
|
+
|
4
|
+
Manages the lifecycle of model instances including:
|
5
|
+
- Lazy loading (models loaded on-demand, not at startup)
|
6
|
+
- Thread-safe access
|
7
|
+
- Usage tracking
|
8
|
+
- Automatic cleanup of inactive models
|
9
|
+
- Memory management
|
10
|
+
"""
|
11
|
+
|
12
|
+
import time
|
13
|
+
import gc
|
14
|
+
from threading import Lock
|
15
|
+
from typing import Dict, List, Optional
|
16
|
+
from .factory import ModelFactory
|
17
|
+
|
18
|
+
|
19
|
+
class ModelManager:
|
20
|
+
"""
|
21
|
+
Manages the lifecycle of model instances.
|
22
|
+
|
23
|
+
Key features:
|
24
|
+
- Lazy loading: Models are only loaded when first requested
|
25
|
+
- Thread-safe: Multiple concurrent requests handled correctly
|
26
|
+
- Usage tracking: Timestamp recorded for each model access
|
27
|
+
- Memory management: Can unload models to free memory
|
28
|
+
|
29
|
+
Similar to the vision engine's dynamic model deployment pattern.
|
30
|
+
"""
|
31
|
+
|
32
|
+
def __init__(self):
|
33
|
+
"""Initialize the model manager."""
|
34
|
+
self._models: Dict[str, object] = {} # model_id → model instance
|
35
|
+
self._last_used: Dict[str, float] = {} # model_id → timestamp
|
36
|
+
self._locks: Dict[str, Lock] = {} # model_id → Lock (for thread-safe loading)
|
37
|
+
self._global_lock = Lock() # Lock for managing _locks dict
|
38
|
+
self._factory = ModelFactory()
|
39
|
+
|
40
|
+
def _get_model_id(self, family: str, variant: str) -> str:
|
41
|
+
"""
|
42
|
+
Generate a unique model ID from family and variant.
|
43
|
+
|
44
|
+
Args:
|
45
|
+
family: Model family name
|
46
|
+
variant: Model variant name
|
47
|
+
|
48
|
+
Returns:
|
49
|
+
str: Unique model identifier in format 'family/variant'
|
50
|
+
"""
|
51
|
+
return f"{family}/{variant}"
|
52
|
+
|
53
|
+
def _parse_model_id(self, model_id: str) -> tuple:
|
54
|
+
"""
|
55
|
+
Parse a model ID into family and variant components.
|
56
|
+
|
57
|
+
Args:
|
58
|
+
model_id: Model identifier in format 'family/variant'
|
59
|
+
|
60
|
+
Returns:
|
61
|
+
tuple: (family, variant)
|
62
|
+
|
63
|
+
Raises:
|
64
|
+
ValueError: If model_id format is invalid
|
65
|
+
"""
|
66
|
+
if '/' not in model_id:
|
67
|
+
raise ValueError(
|
68
|
+
f"Invalid model_id format: '{model_id}'. "
|
69
|
+
f"Expected format: 'family/variant' (e.g., 'detectron2/mask_rcnn_R_50_FPN_3x')"
|
70
|
+
)
|
71
|
+
|
72
|
+
parts = model_id.split('/', 1)
|
73
|
+
return parts[0], parts[1]
|
74
|
+
|
75
|
+
def get_model(self, family: str, variant: str):
|
76
|
+
"""
|
77
|
+
Get a model instance, loading it if necessary (lazy loading).
|
78
|
+
|
79
|
+
This method is thread-safe: if multiple threads request the same model
|
80
|
+
simultaneously, only one will load it while others wait.
|
81
|
+
|
82
|
+
Args:
|
83
|
+
family: Model family name (e.g., 'detectron2', 'depth_anything')
|
84
|
+
variant: Model variant name (e.g., 'mask_rcnn_R_50_FPN_3x', 'small')
|
85
|
+
|
86
|
+
Returns:
|
87
|
+
Model predictor instance
|
88
|
+
|
89
|
+
Raises:
|
90
|
+
ValueError: If family or variant is invalid
|
91
|
+
RuntimeError: If model fails to load
|
92
|
+
|
93
|
+
Example:
|
94
|
+
>>> manager = ModelManager()
|
95
|
+
>>> model = manager.get_model('detectron2', 'mask_rcnn_R_50_FPN_3x')
|
96
|
+
>>> predictions = model.predict(image)
|
97
|
+
"""
|
98
|
+
model_id = self._get_model_id(family, variant)
|
99
|
+
|
100
|
+
# Ensure a lock exists for this model (thread-safe lock creation)
|
101
|
+
with self._global_lock:
|
102
|
+
if model_id not in self._locks:
|
103
|
+
self._locks[model_id] = Lock()
|
104
|
+
|
105
|
+
# Only one thread can load a given model at a time
|
106
|
+
with self._locks[model_id]:
|
107
|
+
# Check if model is already loaded
|
108
|
+
if model_id not in self._models:
|
109
|
+
print(f"[ModelManager] Loading model: {model_id} (family={family}, variant={variant})...")
|
110
|
+
try:
|
111
|
+
self._models[model_id] = self._factory.create_model(family, variant)
|
112
|
+
print(f"[ModelManager] Model {model_id} loaded successfully.")
|
113
|
+
except Exception as e:
|
114
|
+
print(f"[ModelManager] ERROR: Failed to load model {model_id}: {e}")
|
115
|
+
raise RuntimeError(f"Failed to load model {model_id}") from e
|
116
|
+
else:
|
117
|
+
print(f"[ModelManager] Model {model_id} already loaded, reusing existing instance.")
|
118
|
+
|
119
|
+
# Update last used timestamp
|
120
|
+
self._last_used[model_id] = time.time()
|
121
|
+
|
122
|
+
return self._models[model_id]
|
123
|
+
|
124
|
+
def get_model_by_id(self, model_id: str):
|
125
|
+
"""
|
126
|
+
Get a model instance by its full ID (family/variant format).
|
127
|
+
|
128
|
+
Args:
|
129
|
+
model_id: Full model identifier (e.g., 'detectron2/mask_rcnn_R_50_FPN_3x')
|
130
|
+
|
131
|
+
Returns:
|
132
|
+
Model predictor instance
|
133
|
+
|
134
|
+
Raises:
|
135
|
+
ValueError: If model_id format is invalid
|
136
|
+
"""
|
137
|
+
family, variant = self._parse_model_id(model_id)
|
138
|
+
return self.get_model(family, variant)
|
139
|
+
|
140
|
+
def unload_model(self, family: str, variant: str) -> bool:
|
141
|
+
"""
|
142
|
+
Explicitly unload a model to free memory.
|
143
|
+
|
144
|
+
Args:
|
145
|
+
family: Model family name
|
146
|
+
variant: Model variant name
|
147
|
+
|
148
|
+
Returns:
|
149
|
+
bool: True if model was unloaded, False if it wasn't loaded
|
150
|
+
|
151
|
+
Example:
|
152
|
+
>>> manager.unload_model('detectron2', 'mask_rcnn_R_50_FPN_3x')
|
153
|
+
"""
|
154
|
+
model_id = self._get_model_id(family, variant)
|
155
|
+
return self.unload_model_by_id(model_id)
|
156
|
+
|
157
|
+
def unload_model_by_id(self, model_id: str) -> bool:
|
158
|
+
"""
|
159
|
+
Explicitly unload a model by its ID to free memory.
|
160
|
+
|
161
|
+
Args:
|
162
|
+
model_id: Full model identifier
|
163
|
+
|
164
|
+
Returns:
|
165
|
+
bool: True if model was unloaded, False if it wasn't loaded
|
166
|
+
"""
|
167
|
+
if model_id in self._models:
|
168
|
+
print(f"[ModelManager] Unloading model: {model_id}...")
|
169
|
+
del self._models[model_id]
|
170
|
+
if model_id in self._last_used:
|
171
|
+
del self._last_used[model_id]
|
172
|
+
|
173
|
+
# Explicitly trigger garbage collection to free memory
|
174
|
+
gc.collect()
|
175
|
+
|
176
|
+
print(f"[ModelManager] Model {model_id} unloaded successfully.")
|
177
|
+
return True
|
178
|
+
else:
|
179
|
+
print(f"[ModelManager] Model {model_id} not loaded, nothing to unload.")
|
180
|
+
return False
|
181
|
+
|
182
|
+
def list_loaded_models(self) -> List[str]:
|
183
|
+
"""
|
184
|
+
Get list of currently loaded model IDs.
|
185
|
+
|
186
|
+
Returns:
|
187
|
+
list: Model IDs of all loaded models
|
188
|
+
"""
|
189
|
+
return list(self._models.keys())
|
190
|
+
|
191
|
+
def get_inactive_models(self, inactive_seconds: int = 600) -> List[str]:
|
192
|
+
"""
|
193
|
+
Find models that haven't been used in the specified time period.
|
194
|
+
|
195
|
+
Args:
|
196
|
+
inactive_seconds: Time threshold in seconds (default: 600 = 10 minutes)
|
197
|
+
|
198
|
+
Returns:
|
199
|
+
list: Model IDs of inactive models
|
200
|
+
"""
|
201
|
+
current_time = time.time()
|
202
|
+
inactive = []
|
203
|
+
|
204
|
+
for model_id, last_used in self._last_used.items():
|
205
|
+
if current_time - last_used > inactive_seconds:
|
206
|
+
inactive.append(model_id)
|
207
|
+
|
208
|
+
return inactive
|
209
|
+
|
210
|
+
def cleanup_inactive_models(self, inactive_seconds: int = 600) -> int:
|
211
|
+
"""
|
212
|
+
Automatically unload models that haven't been used recently.
|
213
|
+
|
214
|
+
This is similar to the vision engine's automatic model cleanup.
|
215
|
+
|
216
|
+
Args:
|
217
|
+
inactive_seconds: Time threshold in seconds (default: 600 = 10 minutes)
|
218
|
+
|
219
|
+
Returns:
|
220
|
+
int: Number of models unloaded
|
221
|
+
|
222
|
+
Example:
|
223
|
+
>>> # Unload models inactive for more than 10 minutes
|
224
|
+
>>> count = manager.cleanup_inactive_models(600)
|
225
|
+
>>> print(f"Unloaded {count} inactive models")
|
226
|
+
"""
|
227
|
+
inactive_models = self.get_inactive_models(inactive_seconds)
|
228
|
+
count = 0
|
229
|
+
|
230
|
+
for model_id in inactive_models:
|
231
|
+
if self.unload_model_by_id(model_id):
|
232
|
+
count += 1
|
233
|
+
|
234
|
+
if count > 0:
|
235
|
+
print(f"[ModelManager] Cleanup: Unloaded {count} inactive model(s).")
|
236
|
+
|
237
|
+
return count
|
238
|
+
|
239
|
+
def get_model_info(self, model_id: Optional[str] = None) -> dict:
|
240
|
+
"""
|
241
|
+
Get information about loaded models.
|
242
|
+
|
243
|
+
Args:
|
244
|
+
model_id: Optional model ID. If None, returns info about all loaded models
|
245
|
+
|
246
|
+
Returns:
|
247
|
+
dict: Model information including load status and last used time
|
248
|
+
"""
|
249
|
+
if model_id is None:
|
250
|
+
# Return info for all loaded models
|
251
|
+
result = {}
|
252
|
+
current_time = time.time()
|
253
|
+
for mid in self._models.keys():
|
254
|
+
last_used = self._last_used.get(mid, 0)
|
255
|
+
result[mid] = {
|
256
|
+
'loaded': True,
|
257
|
+
'last_used': last_used,
|
258
|
+
'inactive_seconds': int(current_time - last_used) if last_used > 0 else 0
|
259
|
+
}
|
260
|
+
return result
|
261
|
+
else:
|
262
|
+
# Return info for specific model
|
263
|
+
if model_id in self._models:
|
264
|
+
last_used = self._last_used.get(model_id, 0)
|
265
|
+
current_time = time.time()
|
266
|
+
return {
|
267
|
+
'model_id': model_id,
|
268
|
+
'loaded': True,
|
269
|
+
'last_used': last_used,
|
270
|
+
'inactive_seconds': int(current_time - last_used) if last_used > 0 else 0
|
271
|
+
}
|
272
|
+
else:
|
273
|
+
return {
|
274
|
+
'model_id': model_id,
|
275
|
+
'loaded': False
|
276
|
+
}
|
277
|
+
|
278
|
+
def unload_all_models(self) -> int:
|
279
|
+
"""
|
280
|
+
Unload all currently loaded models.
|
281
|
+
|
282
|
+
Useful for cleanup or testing.
|
283
|
+
|
284
|
+
Returns:
|
285
|
+
int: Number of models unloaded
|
286
|
+
"""
|
287
|
+
model_ids = list(self._models.keys())
|
288
|
+
count = 0
|
289
|
+
|
290
|
+
for model_id in model_ids:
|
291
|
+
if self.unload_model_by_id(model_id):
|
292
|
+
count += 1
|
293
|
+
|
294
|
+
return count
|
mozo/registry.py
ADDED
@@ -0,0 +1,235 @@
|
|
1
|
+
"""
|
2
|
+
Model Registry for Mozo
|
3
|
+
|
4
|
+
Centralized registry of all available model families and their variants.
|
5
|
+
This registry maps model families to their adapter classes and supported variants.
|
6
|
+
|
7
|
+
Usage:
|
8
|
+
# To add a new model family, add an entry to MODEL_REGISTRY
|
9
|
+
# To add a new variant to an existing family, add it to the 'variants' dict
|
10
|
+
|
11
|
+
Example:
|
12
|
+
'detectron2': {
|
13
|
+
'adapter_class': 'Detectron2Predictor',
|
14
|
+
'task_type': 'object_detection',
|
15
|
+
'variants': {
|
16
|
+
'mask_rcnn_R_50_FPN_3x': {
|
17
|
+
'variant': 'mask_rcnn_R_50_FPN_3x',
|
18
|
+
'confidence_threshold': 0.5,
|
19
|
+
'device': 'cpu'
|
20
|
+
}
|
21
|
+
}
|
22
|
+
}
|
23
|
+
"""
|
24
|
+
|
25
|
+
# Main model registry - maps family names to adapter configurations
|
26
|
+
MODEL_REGISTRY = {
|
27
|
+
'detectron2': {
|
28
|
+
'adapter_class': 'Detectron2Predictor',
|
29
|
+
'module': 'mozo.adapters.detectron2',
|
30
|
+
'task_type': 'object_detection',
|
31
|
+
'description': 'Detectron2 models for object detection, instance segmentation, and keypoint detection',
|
32
|
+
'variants': {
|
33
|
+
# Mask R-CNN variants (Instance Segmentation)
|
34
|
+
'mask_rcnn_R_50_FPN_3x': {
|
35
|
+
'variant': 'mask_rcnn_R_50_FPN_3x',
|
36
|
+
'confidence_threshold': 0.5,
|
37
|
+
'device': 'cpu'
|
38
|
+
},
|
39
|
+
'mask_rcnn_R_50_C4_1x': {
|
40
|
+
'variant': 'mask_rcnn_R_50_C4_1x',
|
41
|
+
'confidence_threshold': 0.5,
|
42
|
+
'device': 'cpu'
|
43
|
+
},
|
44
|
+
'mask_rcnn_R_50_C4_3x': {
|
45
|
+
'variant': 'mask_rcnn_R_50_C4_3x',
|
46
|
+
'confidence_threshold': 0.5,
|
47
|
+
'device': 'cpu'
|
48
|
+
},
|
49
|
+
'mask_rcnn_R_101_FPN_3x': {
|
50
|
+
'variant': 'mask_rcnn_R_101_FPN_3x',
|
51
|
+
'confidence_threshold': 0.5,
|
52
|
+
'device': 'cpu'
|
53
|
+
},
|
54
|
+
'mask_rcnn_X_101_32x8d_FPN_3x': {
|
55
|
+
'variant': 'mask_rcnn_X_101_32x8d_FPN_3x',
|
56
|
+
'confidence_threshold': 0.5,
|
57
|
+
'device': 'cpu'
|
58
|
+
},
|
59
|
+
|
60
|
+
# Faster R-CNN variants (Object Detection)
|
61
|
+
'faster_rcnn_R_50_FPN_3x': {
|
62
|
+
'variant': 'faster_rcnn_R_50_FPN_3x',
|
63
|
+
'confidence_threshold': 0.7,
|
64
|
+
'device': 'cpu'
|
65
|
+
},
|
66
|
+
'faster_rcnn_R_50_C4_1x': {
|
67
|
+
'variant': 'faster_rcnn_R_50_C4_1x',
|
68
|
+
'confidence_threshold': 0.7,
|
69
|
+
'device': 'cpu'
|
70
|
+
},
|
71
|
+
'faster_rcnn_R_101_FPN_3x': {
|
72
|
+
'variant': 'faster_rcnn_R_101_FPN_3x',
|
73
|
+
'confidence_threshold': 0.7,
|
74
|
+
'device': 'cpu'
|
75
|
+
},
|
76
|
+
'faster_rcnn_X_101_32x8d_FPN_3x': {
|
77
|
+
'variant': 'faster_rcnn_X_101_32x8d_FPN_3x',
|
78
|
+
'confidence_threshold': 0.7,
|
79
|
+
'device': 'cpu'
|
80
|
+
},
|
81
|
+
|
82
|
+
# RetinaNet variants (Object Detection)
|
83
|
+
'retinanet_R_50_FPN_1x': {
|
84
|
+
'variant': 'retinanet_R_50_FPN_1x',
|
85
|
+
'confidence_threshold': 0.5,
|
86
|
+
'device': 'cpu'
|
87
|
+
},
|
88
|
+
'retinanet_R_50_FPN_3x': {
|
89
|
+
'variant': 'retinanet_R_50_FPN_3x',
|
90
|
+
'confidence_threshold': 0.5,
|
91
|
+
'device': 'cpu'
|
92
|
+
},
|
93
|
+
'retinanet_R_101_FPN_3x': {
|
94
|
+
'variant': 'retinanet_R_101_FPN_3x',
|
95
|
+
'confidence_threshold': 0.5,
|
96
|
+
'device': 'cpu'
|
97
|
+
},
|
98
|
+
|
99
|
+
# Keypoint R-CNN variants (Keypoint Detection)
|
100
|
+
'keypoint_rcnn_R_50_FPN_3x': {
|
101
|
+
'variant': 'keypoint_rcnn_R_50_FPN_3x',
|
102
|
+
'confidence_threshold': 0.5,
|
103
|
+
'device': 'cpu'
|
104
|
+
},
|
105
|
+
'keypoint_rcnn_R_101_FPN_3x': {
|
106
|
+
'variant': 'keypoint_rcnn_R_101_FPN_3x',
|
107
|
+
'confidence_threshold': 0.5,
|
108
|
+
'device': 'cpu'
|
109
|
+
},
|
110
|
+
'keypoint_rcnn_X_101_32x8d_FPN_3x': {
|
111
|
+
'variant': 'keypoint_rcnn_X_101_32x8d_FPN_3x',
|
112
|
+
'confidence_threshold': 0.5,
|
113
|
+
'device': 'cpu'
|
114
|
+
},
|
115
|
+
|
116
|
+
# RPN variants (Region Proposal Network)
|
117
|
+
'rpn_R_50_FPN_1x': {
|
118
|
+
'variant': 'rpn_R_50_FPN_1x',
|
119
|
+
'confidence_threshold': 0.5,
|
120
|
+
'device': 'cpu'
|
121
|
+
},
|
122
|
+
|
123
|
+
# Fast R-CNN variants
|
124
|
+
'fast_rcnn_R_50_FPN_1x': {
|
125
|
+
'variant': 'fast_rcnn_R_50_FPN_1x',
|
126
|
+
'confidence_threshold': 0.5,
|
127
|
+
'device': 'cpu'
|
128
|
+
},
|
129
|
+
}
|
130
|
+
},
|
131
|
+
|
132
|
+
'depth_anything': {
|
133
|
+
'adapter_class': 'DepthAnythingPredictor',
|
134
|
+
'module': 'mozo.adapters.depth_anything',
|
135
|
+
'task_type': 'depth_estimation',
|
136
|
+
'description': 'Depth Anything V2 models for monocular depth estimation',
|
137
|
+
'variants': {
|
138
|
+
'small': {
|
139
|
+
'variant': 'small'
|
140
|
+
},
|
141
|
+
'base': {
|
142
|
+
'variant': 'base'
|
143
|
+
},
|
144
|
+
'large': {
|
145
|
+
'variant': 'large'
|
146
|
+
},
|
147
|
+
}
|
148
|
+
},
|
149
|
+
|
150
|
+
'qwen2.5_vl': {
|
151
|
+
'adapter_class': 'Qwen2_5VLPredictor',
|
152
|
+
'module': 'mozo.adapters.qwen2_5_vl',
|
153
|
+
'task_type': 'visual_question_answering',
|
154
|
+
'description': 'Qwen2.5-VL models for vision-language understanding, VQA, and image analysis',
|
155
|
+
'variants': {
|
156
|
+
'7b-instruct': {
|
157
|
+
'variant': '7b-instruct',
|
158
|
+
'device': 'cpu', # MPS has compatibility issues with Qwen2.5-VL
|
159
|
+
'torch_dtype': 'auto'
|
160
|
+
},
|
161
|
+
}
|
162
|
+
},
|
163
|
+
}
|
164
|
+
|
165
|
+
|
166
|
+
def get_available_families():
|
167
|
+
"""
|
168
|
+
Get list of all available model families.
|
169
|
+
|
170
|
+
Returns:
|
171
|
+
list: List of model family names
|
172
|
+
"""
|
173
|
+
return list(MODEL_REGISTRY.keys())
|
174
|
+
|
175
|
+
|
176
|
+
def get_available_variants(family):
|
177
|
+
"""
|
178
|
+
Get list of all available variants for a model family.
|
179
|
+
|
180
|
+
Args:
|
181
|
+
family: Model family name
|
182
|
+
|
183
|
+
Returns:
|
184
|
+
list: List of variant names for the family
|
185
|
+
|
186
|
+
Raises:
|
187
|
+
ValueError: If family is not in registry
|
188
|
+
"""
|
189
|
+
if family not in MODEL_REGISTRY:
|
190
|
+
raise ValueError(f"Unknown model family: '{family}'. Available families: {get_available_families()}")
|
191
|
+
return list(MODEL_REGISTRY[family]['variants'].keys())
|
192
|
+
|
193
|
+
|
194
|
+
def get_model_info(family, variant=None):
|
195
|
+
"""
|
196
|
+
Get detailed information about a model family or specific variant.
|
197
|
+
|
198
|
+
Args:
|
199
|
+
family: Model family name
|
200
|
+
variant: Optional variant name. If None, returns family-level info
|
201
|
+
|
202
|
+
Returns:
|
203
|
+
dict: Model information
|
204
|
+
|
205
|
+
Raises:
|
206
|
+
ValueError: If family or variant is not found
|
207
|
+
"""
|
208
|
+
if family not in MODEL_REGISTRY:
|
209
|
+
raise ValueError(f"Unknown model family: '{family}'. Available families: {get_available_families()}")
|
210
|
+
|
211
|
+
family_config = MODEL_REGISTRY[family]
|
212
|
+
|
213
|
+
if variant is None:
|
214
|
+
# Return family-level info
|
215
|
+
return {
|
216
|
+
'family': family,
|
217
|
+
'adapter_class': family_config['adapter_class'],
|
218
|
+
'task_type': family_config['task_type'],
|
219
|
+
'description': family_config.get('description', ''),
|
220
|
+
'num_variants': len(family_config['variants']),
|
221
|
+
'variants': list(family_config['variants'].keys())
|
222
|
+
}
|
223
|
+
else:
|
224
|
+
# Return variant-specific info
|
225
|
+
if variant not in family_config['variants']:
|
226
|
+
raise ValueError(
|
227
|
+
f"Unknown variant '{variant}' for family '{family}'. "
|
228
|
+
f"Available variants: {list(family_config['variants'].keys())}"
|
229
|
+
)
|
230
|
+
return {
|
231
|
+
'family': family,
|
232
|
+
'variant': variant,
|
233
|
+
'task_type': family_config['task_type'],
|
234
|
+
'parameters': family_config['variants'][variant]
|
235
|
+
}
|