endoreg-db 0.8.3.7__py3-none-any.whl → 0.8.6.3__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.
Files changed (41) hide show
  1. endoreg_db/data/ai_model_meta/default_multilabel_classification.yaml +23 -1
  2. endoreg_db/data/setup_config.yaml +38 -0
  3. endoreg_db/management/commands/create_model_meta_from_huggingface.py +19 -5
  4. endoreg_db/management/commands/load_ai_model_data.py +18 -15
  5. endoreg_db/management/commands/setup_endoreg_db.py +218 -33
  6. endoreg_db/models/media/pdf/raw_pdf.py +241 -97
  7. endoreg_db/models/media/video/pipe_1.py +30 -33
  8. endoreg_db/models/media/video/video_file.py +300 -187
  9. endoreg_db/models/medical/hardware/endoscopy_processor.py +10 -1
  10. endoreg_db/models/metadata/model_meta_logic.py +63 -43
  11. endoreg_db/models/metadata/sensitive_meta_logic.py +251 -25
  12. endoreg_db/serializers/__init__.py +26 -55
  13. endoreg_db/serializers/misc/__init__.py +1 -1
  14. endoreg_db/serializers/misc/file_overview.py +65 -35
  15. endoreg_db/serializers/misc/{vop_patient_data.py → sensitive_patient_data.py} +1 -1
  16. endoreg_db/serializers/video_examination.py +198 -0
  17. endoreg_db/services/lookup_service.py +228 -58
  18. endoreg_db/services/lookup_store.py +174 -30
  19. endoreg_db/services/pdf_import.py +585 -282
  20. endoreg_db/services/video_import.py +485 -242
  21. endoreg_db/urls/__init__.py +36 -23
  22. endoreg_db/urls/label_video_segments.py +2 -0
  23. endoreg_db/urls/media.py +3 -2
  24. endoreg_db/utils/setup_config.py +177 -0
  25. endoreg_db/views/__init__.py +5 -3
  26. endoreg_db/views/media/pdf_media.py +3 -1
  27. endoreg_db/views/media/video_media.py +1 -1
  28. endoreg_db/views/media/video_segments.py +187 -259
  29. endoreg_db/views/pdf/__init__.py +5 -8
  30. endoreg_db/views/pdf/pdf_stream.py +187 -0
  31. endoreg_db/views/pdf/reimport.py +110 -94
  32. endoreg_db/views/requirement/lookup.py +171 -287
  33. endoreg_db/views/video/__init__.py +0 -2
  34. endoreg_db/views/video/video_examination_viewset.py +202 -289
  35. {endoreg_db-0.8.3.7.dist-info → endoreg_db-0.8.6.3.dist-info}/METADATA +1 -2
  36. {endoreg_db-0.8.3.7.dist-info → endoreg_db-0.8.6.3.dist-info}/RECORD +38 -37
  37. endoreg_db/views/pdf/pdf_media.py +0 -239
  38. endoreg_db/views/pdf/pdf_stream_views.py +0 -127
  39. endoreg_db/views/video/video_media.py +0 -158
  40. {endoreg_db-0.8.3.7.dist-info → endoreg_db-0.8.6.3.dist-info}/WHEEL +0 -0
  41. {endoreg_db-0.8.3.7.dist-info → endoreg_db-0.8.6.3.dist-info}/licenses/LICENSE +0 -0
@@ -1,5 +1,27 @@
1
+ # Model metadata configuration with setup hints
1
2
  - model: endoreg_db.model_meta
2
3
  fields:
3
4
  name: "image_multilabel_classification_colonoscopy_default"
4
- version: 1
5
+ version: "1"
6
+ model: "image_multilabel_classification_colonoscopy_default"
5
7
  labelset: "multilabel_classification_colonoscopy_default"
8
+ activation: "sigmoid"
9
+ mean: "0.485,0.456,0.406" # ImageNet defaults
10
+ std: "0.229,0.224,0.225" # ImageNet defaults
11
+ size_x: 224
12
+ size_y: 224
13
+ axes: "CHW"
14
+ batchsize: 32
15
+ num_workers: 4
16
+ description: "Default colonoscopy image multilabel classification model from YAML"
17
+ # Note: weights field intentionally left empty - will be set by setup command or model meta logic
18
+
19
+ # Setup configuration hints for this model
20
+ setup_config:
21
+ is_primary_model: true # Mark this as the primary model for setup
22
+ weight_filenames: # Specific weight file patterns for this model
23
+ - "colo_segmentation_RegNetX800MF_6.ckpt"
24
+ - "image_multilabel_classification_colonoscopy_default_v1_*.ckpt"
25
+ huggingface_fallback: # HF configuration for this model
26
+ repo_id: "wg-lux/colo_segmentation_RegNetX800MF_base"
27
+ filename: "colo_segmentation_RegNetX800MF_base.ckpt"
@@ -0,0 +1,38 @@
1
+ # EndoReg DB Setup Configuration
2
+ # This file defines which models should be used for setup and their fallback configurations
3
+
4
+ default_models:
5
+ primary_classification_model: "image_multilabel_classification_colonoscopy_default"
6
+ primary_labelset: "multilabel_classification_colonoscopy_default"
7
+
8
+ # HuggingFace fallback configuration
9
+ huggingface_fallback:
10
+ enabled: true
11
+ repo_id: "wg-lux/colo_segmentation_RegNetX800MF_base"
12
+ filename: "colo_segmentation_RegNetX800MF_base.ckpt"
13
+ labelset_name: "multilabel_classification_colonoscopy_default"
14
+
15
+ # Local weights search patterns
16
+ weights_search_patterns:
17
+ # Primary model weight filename patterns (supports wildcards)
18
+ - "colo_segmentation_RegNetX800MF_*.ckpt"
19
+ - "image_multilabel_classification_colonoscopy_default_*.ckpt"
20
+ - "*_colonoscopy_*.ckpt"
21
+
22
+ # Search directories for local weights (relative to project root)
23
+ weights_search_dirs:
24
+ - "tests/assets"
25
+ - "assets"
26
+ - "data/storage/model_weights"
27
+ - "${STORAGE_DIR}/model_weights" # Environment variable substitution
28
+
29
+ # Auto-generation defaults when creating metadata
30
+ auto_generation_defaults:
31
+ activation: "sigmoid"
32
+ mean: "0.485,0.456,0.406" # ImageNet defaults
33
+ std: "0.229,0.224,0.225" # ImageNet defaults
34
+ size_x: 224
35
+ size_y: 224
36
+ axes: "CHW"
37
+ batchsize: 32
38
+ num_workers: 4
@@ -50,11 +50,17 @@ class Command(BaseCommand):
50
50
 
51
51
  try:
52
52
  # Download the model weights
53
- weights_path = hf_hub_download(repo_id=model_id, filename="pytorch_model.bin", local_dir="/tmp")
53
+ weights_path = hf_hub_download(
54
+ repo_id=model_id,
55
+ filename="colo_segmentation_RegNetX800MF_base.ckpt",
56
+ local_dir="/tmp",
57
+ )
54
58
  self.stdout.write(f"Downloaded weights to: {weights_path}")
55
59
 
56
60
  # Get or create AI model
57
- ai_model, created = AiModel.objects.get_or_create(name=model_name, defaults={"description": f"Model from {model_id}"})
61
+ ai_model, created = AiModel.objects.get_or_create(
62
+ name=model_name, defaults={"description": f"Model from {model_id}"}
63
+ )
58
64
  if created:
59
65
  self.stdout.write(f"Created AI model: {ai_model.name}")
60
66
 
@@ -62,7 +68,9 @@ class Command(BaseCommand):
62
68
  try:
63
69
  labelset = LabelSet.objects.get(name=labelset_name)
64
70
  except LabelSet.DoesNotExist:
65
- self.stdout.write(self.style.ERROR(f"LabelSet '{labelset_name}' not found"))
71
+ self.stdout.write(
72
+ self.style.ERROR(f"LabelSet '{labelset_name}' not found")
73
+ )
66
74
  return
67
75
 
68
76
  # Create ModelMeta
@@ -86,13 +94,19 @@ class Command(BaseCommand):
86
94
 
87
95
  # Save the weights file to the model
88
96
  with open(weights_path, "rb") as f:
89
- model_meta.weights.save(f"{model_name}_v{version}_pytorch_model.bin", ContentFile(f.read()))
97
+ model_meta.weights.save(
98
+ f"{model_name}_v{version}_pytorch_model.bin", ContentFile(f.read())
99
+ )
90
100
 
91
101
  # Set as active meta
92
102
  ai_model.active_meta = model_meta
93
103
  ai_model.save()
94
104
 
95
- self.stdout.write(self.style.SUCCESS(f"Successfully {'created' if created else 'updated'} ModelMeta: {model_meta}"))
105
+ self.stdout.write(
106
+ self.style.SUCCESS(
107
+ f"Successfully {'created' if created else 'updated'} ModelMeta: {model_meta}"
108
+ )
109
+ )
96
110
 
97
111
  except Exception as e:
98
112
  self.stdout.write(self.style.ERROR(f"Error creating ModelMeta: {e}"))
@@ -1,25 +1,28 @@
1
1
  from django.core.management.base import BaseCommand
2
+
3
+ from ...data import (
4
+ AI_MODEL_DATA_DIR,
5
+ AI_MODEL_META_DATA_DIR, # Add this import
6
+ MODEL_TYPE_DATA_DIR,
7
+ VIDEO_SEGMENTATION_LABEL_DATA_DIR,
8
+ VIDEO_SEGMENTATION_LABELSET_DATA_DIR,
9
+ )
2
10
  from ...models import (
11
+ AiModel,
12
+ LabelSet, # Add LabelSet import
13
+ ModelMeta, # Add ModelMeta back to imports
3
14
  ModelType,
4
15
  VideoSegmentationLabel,
5
16
  VideoSegmentationLabelSet,
6
- AiModel,
7
17
  )
8
18
  from ...utils import load_model_data_from_yaml
9
- from ...data import (
10
- MODEL_TYPE_DATA_DIR,
11
- VIDEO_SEGMENTATION_LABEL_DATA_DIR,
12
- VIDEO_SEGMENTATION_LABELSET_DATA_DIR,
13
- AI_MODEL_DATA_DIR,
14
- )
15
-
16
19
 
17
20
  IMPORT_MODELS = [ # string as model key, serves as key in IMPORT_METADATA
18
21
  ModelType.__name__,
19
- # ModelMeta.__name__,
20
22
  VideoSegmentationLabel.__name__,
21
23
  VideoSegmentationLabelSet.__name__,
22
24
  AiModel.__name__,
25
+ ModelMeta.__name__, # Re-enable ModelMeta loading
23
26
  ]
24
27
 
25
28
  IMPORT_METADATA = {
@@ -29,12 +32,12 @@ IMPORT_METADATA = {
29
32
  "foreign_keys": [], # e.g. ["interventions"]
30
33
  "foreign_key_models": [], # e.g. [Intervention]
31
34
  },
32
- # ModelMeta.__name__: {
33
- # "dir": AI_MODEL_META_DATA_DIR, # e.g. "intervention_types"
34
- # "model": ModelMeta, # e.g. InterventionType
35
- # "foreign_keys": ["labelset", "type"], # e.g. ["interventions"]
36
- # "foreign_key_models": [LabelSet, ModelType], # e.g. [Intervention]
37
- # },
35
+ ModelMeta.__name__: {
36
+ "dir": AI_MODEL_META_DATA_DIR, # e.g. "ai_model_meta"
37
+ "model": ModelMeta, # e.g. ModelMeta
38
+ "foreign_keys": ["labelset", "model"], # Foreign key relationships
39
+ "foreign_key_models": [LabelSet, AiModel], # Actual model classes
40
+ },
38
41
  VideoSegmentationLabel.__name__: {
39
42
  "dir": VIDEO_SEGMENTATION_LABEL_DATA_DIR, # e.g. "interventions"
40
43
  "model": VideoSegmentationLabel, # e.g. Intervention
@@ -3,21 +3,21 @@ Django management command to perform complete setup for EndoReg DB when used as
3
3
  This command ensures all necessary data and configurations are initialized.
4
4
  """
5
5
 
6
- import os
7
- from pathlib import Path
8
-
9
6
  from django.core.management import call_command
10
7
  from django.core.management.base import BaseCommand
11
8
 
9
+ from endoreg_db.models import ModelMeta
10
+
12
11
 
13
12
  class Command(BaseCommand):
14
13
  help = """
15
14
  Complete setup for EndoReg DB when used as an embedded app.
16
15
  This command performs all necessary initialization steps:
17
16
  1. Loads base database data
18
- 2. Sets up AI models and labels
19
- 3. Creates cache table
20
- 4. Initializes model metadata
17
+ 2. Sets up caching (if using db cache)
18
+ 3. Loads default models from setup configuration file (setup_config.yaml)
19
+ 4. Loads models according to fallback chain (Local Files -> HuggingFace -> graceful failure)
20
+ 5. Initializes model metadata
21
21
  """
22
22
 
23
23
  def add_arguments(self, parser):
@@ -31,13 +31,22 @@ class Command(BaseCommand):
31
31
  action="store_true",
32
32
  help="Force recreation of AI model metadata even if it exists",
33
33
  )
34
+ parser.add_argument(
35
+ "--yaml-only",
36
+ action="store_true",
37
+ help="Only use YAML-defined models, don't auto-generate missing metadata",
38
+ )
34
39
 
35
40
  def handle(self, *args, **options):
36
41
  skip_ai = options.get("skip_ai_setup", False)
37
42
  force_recreate = options.get("force_recreate", False)
43
+ yaml_only = options.get("yaml_only", False)
38
44
 
39
45
  self.stdout.write(self.style.SUCCESS("🚀 Starting EndoReg DB embedded app setup..."))
40
46
 
47
+ if yaml_only:
48
+ self.stdout.write(self.style.WARNING("📋 YAML-only mode: Will not auto-generate missing metadata"))
49
+
41
50
  # Step 1: Load base database data
42
51
  self.stdout.write("\n📊 Step 1: Loading base database data...")
43
52
  try:
@@ -47,7 +56,7 @@ class Command(BaseCommand):
47
56
  self.stdout.write(self.style.ERROR(f"❌ Failed to load base data: {e}"))
48
57
  return
49
58
 
50
- # Step 2: Create cache table (only if using database caching)
59
+ # Step 2: Create cache table (only if using database caching)
51
60
  self.stdout.write("\n💾 Step 2: Setting up caching...")
52
61
  from django.conf import settings
53
62
 
@@ -87,10 +96,16 @@ class Command(BaseCommand):
87
96
  # Step 5: Create model metadata
88
97
  self.stdout.write("\n📋 Step 5: Creating AI model metadata...")
89
98
  try:
99
+ # Load setup configuration
100
+ from endoreg_db.utils.setup_config import setup_config
101
+
102
+ # Get primary model from configuration
103
+ default_model_name = setup_config.get_primary_model_name()
104
+ primary_labelset = setup_config.get_primary_labelset_name()
105
+
90
106
  # Check if model metadata already exists
91
107
  from endoreg_db.models import AiModel
92
108
 
93
- default_model_name = "image_multilabel_classification_colonoscopy_default"
94
109
  ai_model = AiModel.objects.filter(name=default_model_name).first()
95
110
 
96
111
  if not ai_model:
@@ -101,16 +116,20 @@ class Command(BaseCommand):
101
116
  if existing_meta and not force_recreate:
102
117
  self.stdout.write(self.style.SUCCESS("✅ Model metadata already exists (use --force-recreate to recreate)"))
103
118
  else:
104
- # Try to create model metadata
119
+ # Try to create model metadata using configurable approach
105
120
  model_path = self._find_model_weights_file()
106
121
  if model_path:
107
- call_command(
108
- "create_multilabel_model_meta",
109
- model_name=default_model_name,
110
- model_meta_version=1,
111
- image_classification_labelset_name="multilabel_classification_colonoscopy_default",
112
- model_path=str(model_path),
113
- )
122
+ call_command_kwargs = {
123
+ "model_name": default_model_name,
124
+ "model_meta_version": 1,
125
+ "image_classification_labelset_name": primary_labelset,
126
+ "model_path": str(model_path),
127
+ }
128
+ # Add bump_version flag if force_recreate is enabled
129
+ if force_recreate:
130
+ call_command_kwargs["bump_version"] = True
131
+
132
+ call_command("create_multilabel_model_meta", **call_command_kwargs)
114
133
  self.stdout.write(self.style.SUCCESS("✅ AI model metadata created successfully"))
115
134
  else:
116
135
  self.stdout.write(self.style.WARNING("⚠️ Model weights file not found. AI features may not work properly."))
@@ -119,6 +138,15 @@ class Command(BaseCommand):
119
138
  self.stdout.write(self.style.ERROR(f"❌ Failed to create AI model metadata: {e}"))
120
139
  return
121
140
 
141
+ # Step 5.5: Validate and fix AI model active metadata
142
+ self.stdout.write("\n🔧 Step 5.5: Validating AI model active metadata...")
143
+ try:
144
+ self._validate_and_fix_ai_model_metadata(yaml_only)
145
+ self.stdout.write(self.style.SUCCESS("✅ AI model metadata validation completed"))
146
+ except Exception as e:
147
+ self.stdout.write(self.style.ERROR(f"❌ Failed to validate AI model metadata: {e}"))
148
+ return
149
+
122
150
  # Step 6: Verification
123
151
  self.stdout.write("\n🔍 Step 6: Verifying setup...")
124
152
  try:
@@ -135,25 +163,37 @@ class Command(BaseCommand):
135
163
  self.stdout.write("3. Start development server: python manage.py runserver")
136
164
 
137
165
  def _find_model_weights_file(self):
138
- """Find the model weights file in various possible locations."""
139
- # Check common locations for model weights
140
- possible_paths = [
141
- # Test assets (for development)
142
- Path("tests/assets/colo_segmentation_RegNetX800MF_6.ckpt"),
143
- # Project root assets
144
- Path("assets/colo_segmentation_RegNetX800MF_6.ckpt"),
145
- # Storage directory
146
- Path("data/storage/model_weights/colo_segmentation_RegNetX800MF_6.ckpt"),
147
- # Absolute paths based on environment
148
- Path(os.getenv("STORAGE_DIR", "storage")) / "model_weights" / "colo_segmentation_RegNetX800MF_6.ckpt",
149
- ]
166
+ """Find the model weights file using configurable search patterns and directories."""
167
+ # Load setup configuration
168
+ from endoreg_db.utils.setup_config import setup_config
169
+
170
+ # First try to find weights using configured patterns
171
+ found_files = setup_config.find_model_weights_files()
172
+ if found_files:
173
+ self.stdout.write(f"Found model weights at: {found_files[0]}")
174
+ return found_files[0]
175
+
176
+ # If no local weights found and HuggingFace fallback is enabled
177
+ hf_config = setup_config.get_huggingface_config()
178
+ if hf_config.get("enabled", True):
179
+ self.stdout.write("📦 No local model weights found — attempting HuggingFace download...")
180
+ try:
181
+ if not ModelMeta.objects.exists():
182
+ ModelMeta.setup_default_from_huggingface(
183
+ hf_config.get("repo_id", "wg-lux/colo_segmentation_RegNetX800MF_base"),
184
+ labelset_name=hf_config.get("labelset_name", "multilabel_classification_colonoscopy_default"),
185
+ )
186
+ self.stdout.write("✅ Default ModelMeta created from HuggingFace.")
187
+
188
+ # Try to find the downloaded weights
189
+ found_files = setup_config.find_model_weights_files()
190
+ if found_files:
191
+ return found_files[0]
150
192
 
151
- for path in possible_paths:
152
- if path.exists():
153
- self.stdout.write(f"Found model weights at: {path}")
154
- return path
193
+ except Exception as e:
194
+ self.stdout.write(f"⚠️ HuggingFace download failed: {e}")
155
195
 
156
- self.stdout.write("Model weights file not found in standard locations")
196
+ self.stdout.write("Model weights file not found in configured locations")
157
197
  return None
158
198
 
159
199
  def _verify_setup(self):
@@ -194,3 +234,148 @@ class Command(BaseCommand):
194
234
  self.stdout.write(f"Found {meta_count} model metadata record(s)")
195
235
 
196
236
  self.stdout.write("Setup verification passed")
237
+
238
+ def _validate_and_fix_ai_model_metadata(self, yaml_only=False):
239
+ """
240
+ Validate that all AI models have proper active metadata and fix if necessary.
241
+ This addresses the "No model metadata found for this model" error.
242
+
243
+ Args:
244
+ yaml_only (bool): If True, only set active metadata but don't create new metadata
245
+ """
246
+ from endoreg_db.models import AiModel, LabelSet, ModelMeta
247
+ from endoreg_db.utils.setup_config import setup_config
248
+
249
+ all_models = AiModel.objects.all()
250
+ fixed_count = 0
251
+
252
+ # Get configurable defaults
253
+ defaults = setup_config.get_auto_generation_defaults()
254
+ primary_labelset_name = setup_config.get_primary_labelset_name()
255
+
256
+ for model in all_models:
257
+ self.stdout.write(f"Checking model: {model.name}")
258
+
259
+ # Check if model has metadata versions
260
+ metadata_count = model.metadata_versions.count()
261
+ self.stdout.write(f" Metadata versions: {metadata_count}")
262
+
263
+ if metadata_count == 0:
264
+ if yaml_only:
265
+ self.stdout.write(f" ⚠️ YAML-only mode: Skipping auto-generation for {model.name}")
266
+ continue
267
+
268
+ # Create metadata for models that don't have any
269
+ self.stdout.write(f" Creating metadata for {model.name}...")
270
+
271
+ # Use configured labelset or create default
272
+ labelset = None
273
+ try:
274
+ labelset = LabelSet.objects.get(name=primary_labelset_name)
275
+ except LabelSet.DoesNotExist:
276
+ labelset = LabelSet.objects.first()
277
+ if not labelset:
278
+ labelset = LabelSet.objects.create(name="default_colonoscopy_labels", description="Default colonoscopy classification labels")
279
+
280
+ # Create basic metadata WITH weights if available
281
+ weights_file = self._find_model_weights_file()
282
+ weights_path = ""
283
+ if weights_file:
284
+ # If we have weights, set up the relative path
285
+ from pathlib import Path
286
+
287
+ from endoreg_db.utils.paths import STORAGE_DIR
288
+
289
+ try:
290
+ weights_path = str(Path(weights_file).relative_to(STORAGE_DIR))
291
+ except ValueError:
292
+ # If file is not in storage dir, copy it there
293
+ import shutil
294
+
295
+ weights_dir = STORAGE_DIR / "model_weights"
296
+ weights_dir.mkdir(parents=True, exist_ok=True)
297
+ dest_path = weights_dir / Path(weights_file).name
298
+ shutil.copy2(weights_file, dest_path)
299
+ weights_path = str(dest_path.relative_to(STORAGE_DIR))
300
+ self.stdout.write(f" Copied weights to: {dest_path}")
301
+
302
+ # Create basic metadata using configurable defaults
303
+ meta = ModelMeta.objects.create(
304
+ name=model.name,
305
+ version="1.0",
306
+ model=model,
307
+ labelset=labelset,
308
+ weights=weights_path, # Set weights if available
309
+ activation=defaults.get("activation", "sigmoid"),
310
+ mean=defaults.get("mean", "0.485,0.456,0.406"),
311
+ std=defaults.get("std", "0.229,0.224,0.225"),
312
+ size_x=defaults.get("size_x", 224),
313
+ size_y=defaults.get("size_y", 224),
314
+ axes=defaults.get("axes", "CHW"),
315
+ batchsize=defaults.get("batchsize", 32),
316
+ num_workers=defaults.get("num_workers", 4),
317
+ description=f"Auto-generated metadata for {model.name}",
318
+ )
319
+
320
+ model.active_meta = meta
321
+ model.save()
322
+ fixed_count += 1
323
+ self.stdout.write(f" ✅ Created and set metadata for {model.name}")
324
+
325
+ elif not model.active_meta:
326
+ # Model has metadata but no active meta set
327
+ first_meta = model.metadata_versions.first()
328
+ if first_meta:
329
+ self.stdout.write(f" Setting active metadata for {model.name}...")
330
+
331
+ # Check if the metadata has weights - if not, try to assign them
332
+ if not first_meta.weights:
333
+ self.stdout.write(" Metadata exists but no weights assigned, attempting to add weights...")
334
+ weights_file = self._find_model_weights_file()
335
+ if weights_file:
336
+ from pathlib import Path
337
+
338
+ from endoreg_db.utils.paths import STORAGE_DIR
339
+
340
+ try:
341
+ weights_path = str(Path(weights_file).relative_to(STORAGE_DIR))
342
+ except ValueError:
343
+ # Copy weights to storage if not already there
344
+ import shutil
345
+
346
+ weights_dir = STORAGE_DIR / "model_weights"
347
+ weights_dir.mkdir(parents=True, exist_ok=True)
348
+ dest_path = weights_dir / Path(weights_file).name
349
+ shutil.copy2(weights_file, dest_path)
350
+ weights_path = str(dest_path.relative_to(STORAGE_DIR))
351
+ self.stdout.write(f" Copied weights to: {dest_path}")
352
+
353
+ # Assign the relative path to the FileField
354
+ first_meta.weights.name = weights_path
355
+ first_meta.save(update_fields=["weights"])
356
+ self.stdout.write(f" Added weights to existing metadata: {weights_path}")
357
+
358
+ model.active_meta = first_meta
359
+ model.save()
360
+ fixed_count += 1
361
+ self.stdout.write(f" ✅ Set active metadata: {first_meta.name} v{first_meta.version}")
362
+ else:
363
+ self.stdout.write(f" ⚠️ No metadata versions available for {model.name}")
364
+
365
+ else:
366
+ self.stdout.write(f" ✅ Model {model.name} has active metadata: {model.active_meta}")
367
+
368
+ # Verify all models can get latest version
369
+ self.stdout.write("\nTesting model metadata access...")
370
+ for model in all_models:
371
+ try:
372
+ latest = model.get_latest_version()
373
+ self.stdout.write(f" ✅ {model.name}: {latest}")
374
+ except Exception as e:
375
+ self.stdout.write(f" ❌ {model.name}: {e}")
376
+ raise Exception(f"Model {model.name} still has metadata issues: {e}")
377
+
378
+ if fixed_count > 0:
379
+ self.stdout.write(f"Fixed metadata for {fixed_count} model(s)")
380
+ else:
381
+ self.stdout.write("All models already had proper metadata")