torch-deepaudioembedding 0.1.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.
- torch_dae/__init__.py +10 -0
- torch_dae/cards/__init__.py +59 -0
- torch_dae/cards/models.py +471 -0
- torch_dae/cards/validation.py +77 -0
- torch_dae/cli/__init__.py +1 -0
- torch_dae/cli/cards.py +45 -0
- torch_dae/cli/checkpoints.py +75 -0
- torch_dae/cli/environment.py +177 -0
- torch_dae/cli/main.py +20 -0
- torch_dae/cli/models.py +27 -0
- torch_dae/contracts.py +96 -0
- torch_dae/core/__init__.py +41 -0
- torch_dae/core/capabilities.py +61 -0
- torch_dae/core/checkpoint.py +1386 -0
- torch_dae/core/embeddings.py +62 -0
- torch_dae/core/errors.py +89 -0
- torch_dae/core/model.py +285 -0
- torch_dae/core/outputs.py +116 -0
- torch_dae/core/preprocessing.py +29 -0
- torch_dae/core/registry.py +174 -0
- torch_dae/environment/__init__.py +69 -0
- torch_dae/environment/fingerprint.py +159 -0
- torch_dae/environment/manager.py +1533 -0
- torch_dae/environment/materialization.py +14 -0
- torch_dae/environment/policy.py +25 -0
- torch_dae/environment/runtime.py +286 -0
- torch_dae/environment/sources.py +557 -0
- torch_dae/environment/specification.py +188 -0
- torch_dae/environment/subprocess.py +261 -0
- torch_dae/environment/verification.py +68 -0
- torch_dae/onboarding/__init__.py +87 -0
- torch_dae/onboarding/contracts.py +1336 -0
- torch_dae/onboarding/evaluation.py +423 -0
- torch_dae/onboarding/inspection.py +2387 -0
- torch_dae/onboarding/rendering.py +134 -0
- torch_dae/py.typed +0 -0
- torch_deepaudioembedding-0.1.0.dist-info/METADATA +332 -0
- torch_deepaudioembedding-0.1.0.dist-info/RECORD +42 -0
- torch_deepaudioembedding-0.1.0.dist-info/WHEEL +4 -0
- torch_deepaudioembedding-0.1.0.dist-info/entry_points.txt +2 -0
- torch_deepaudioembedding-0.1.0.dist-info/licenses/LICENSE +201 -0
- torch_deepaudioembedding-0.1.0.dist-info/licenses/NOTICE +2 -0
torch_dae/__init__.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""Root control-plane package for torch-dae.
|
|
2
|
+
|
|
3
|
+
repository foundation exposes contracts and registry helpers without importing PyTorch.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from torch_dae.core.registry import ModelCardRegistry
|
|
7
|
+
|
|
8
|
+
__all__ = ["ModelCardRegistry", "__version__"]
|
|
9
|
+
|
|
10
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Stable, model-agnostic model-card contract namespace."""
|
|
2
|
+
|
|
3
|
+
from torch_dae.cards.models import (
|
|
4
|
+
BooleanCapability,
|
|
5
|
+
CapabilitiesSection,
|
|
6
|
+
DatasetRecord,
|
|
7
|
+
Datasets,
|
|
8
|
+
Description,
|
|
9
|
+
DeviceSupport,
|
|
10
|
+
EmbeddingsSection,
|
|
11
|
+
EvidenceRecord,
|
|
12
|
+
EvidenceStatus,
|
|
13
|
+
Identity,
|
|
14
|
+
IssueRecord,
|
|
15
|
+
IssueStatus,
|
|
16
|
+
MetricRecord,
|
|
17
|
+
ModelCard,
|
|
18
|
+
ModelCardLifecycle,
|
|
19
|
+
OutputComponent,
|
|
20
|
+
Outputs,
|
|
21
|
+
ProfilingSection,
|
|
22
|
+
ProfilingStatus,
|
|
23
|
+
RecommendedEnvironment,
|
|
24
|
+
ScientificReference,
|
|
25
|
+
SourceRecord,
|
|
26
|
+
Sources,
|
|
27
|
+
Tasks,
|
|
28
|
+
Usage,
|
|
29
|
+
WaveformInput,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
"BooleanCapability",
|
|
34
|
+
"CapabilitiesSection",
|
|
35
|
+
"DatasetRecord",
|
|
36
|
+
"Datasets",
|
|
37
|
+
"Description",
|
|
38
|
+
"DeviceSupport",
|
|
39
|
+
"EmbeddingsSection",
|
|
40
|
+
"EvidenceRecord",
|
|
41
|
+
"EvidenceStatus",
|
|
42
|
+
"Identity",
|
|
43
|
+
"IssueRecord",
|
|
44
|
+
"IssueStatus",
|
|
45
|
+
"MetricRecord",
|
|
46
|
+
"ModelCard",
|
|
47
|
+
"ModelCardLifecycle",
|
|
48
|
+
"OutputComponent",
|
|
49
|
+
"Outputs",
|
|
50
|
+
"ProfilingSection",
|
|
51
|
+
"ProfilingStatus",
|
|
52
|
+
"RecommendedEnvironment",
|
|
53
|
+
"ScientificReference",
|
|
54
|
+
"SourceRecord",
|
|
55
|
+
"Sources",
|
|
56
|
+
"Tasks",
|
|
57
|
+
"Usage",
|
|
58
|
+
"WaveformInput",
|
|
59
|
+
]
|
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
"""Strict model-card contracts for repository foundation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from enum import StrEnum
|
|
6
|
+
from typing import Annotated, Literal
|
|
7
|
+
|
|
8
|
+
from pydantic import Field, HttpUrl, field_validator, model_validator
|
|
9
|
+
|
|
10
|
+
from torch_dae.contracts import (
|
|
11
|
+
GIT_REVISION_PATTERN,
|
|
12
|
+
REPO_RELATIVE_PATTERN,
|
|
13
|
+
CanonicalId,
|
|
14
|
+
StrictBaseModel,
|
|
15
|
+
ensure_repository_relative,
|
|
16
|
+
ensure_wrapper_entry_point,
|
|
17
|
+
)
|
|
18
|
+
from torch_dae.core.checkpoint import CheckpointSpec
|
|
19
|
+
from torch_dae.core.embeddings import EmbeddingSpec
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ModelCardLifecycle(StrEnum):
|
|
23
|
+
"""Lifecycle states defined by project_spec.md."""
|
|
24
|
+
|
|
25
|
+
DRAFT = "draft"
|
|
26
|
+
ANALYZED = "analyzed"
|
|
27
|
+
ENVIRONMENT_RESOLVED = "environment_resolved"
|
|
28
|
+
CHECKPOINT_VERIFIED = "checkpoint_verified"
|
|
29
|
+
RUNTIME_VERIFIED = "runtime_verified"
|
|
30
|
+
PROFILED = "profiled"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class EvidenceStatus(StrEnum):
|
|
34
|
+
"""Evidence provenance status."""
|
|
35
|
+
|
|
36
|
+
OFFICIALLY_REPORTED = "officially_reported"
|
|
37
|
+
OBSERVED = "observed"
|
|
38
|
+
INFERRED = "inferred"
|
|
39
|
+
UNRESOLVED = "unresolved"
|
|
40
|
+
NOT_REPORTED = "not_reported"
|
|
41
|
+
NOT_APPLICABLE = "not_applicable"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class IssueStatus(StrEnum):
|
|
45
|
+
"""Issue lifecycle state."""
|
|
46
|
+
|
|
47
|
+
OPEN = "open"
|
|
48
|
+
RESOLVED = "resolved"
|
|
49
|
+
ACCEPTED = "accepted"
|
|
50
|
+
NOT_APPLICABLE = "not_applicable"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class ProfilingStatus(StrEnum):
|
|
54
|
+
"""Profiling status reserved for later phases."""
|
|
55
|
+
|
|
56
|
+
NOT_PROFILED = "not_profiled"
|
|
57
|
+
PROFILED = "profiled"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class EvidenceRecord(StrictBaseModel):
|
|
61
|
+
"""Concise evidence record for a material claim."""
|
|
62
|
+
|
|
63
|
+
evidence_id: CanonicalId
|
|
64
|
+
kind: str
|
|
65
|
+
status: EvidenceStatus
|
|
66
|
+
url: HttpUrl | None = None
|
|
67
|
+
revision: Annotated[str | None, Field(pattern=GIT_REVISION_PATTERN)] = None
|
|
68
|
+
path: str | None = None
|
|
69
|
+
symbol: str | None = None
|
|
70
|
+
description: str
|
|
71
|
+
rationale: str | None = None
|
|
72
|
+
|
|
73
|
+
@model_validator(mode="after")
|
|
74
|
+
def inferred_requires_rationale(self) -> EvidenceRecord:
|
|
75
|
+
if self.status == EvidenceStatus.INFERRED and not self.rationale:
|
|
76
|
+
raise ValueError("inferred evidence requires rationale")
|
|
77
|
+
return self
|
|
78
|
+
|
|
79
|
+
@field_validator("path")
|
|
80
|
+
@classmethod
|
|
81
|
+
def path_repository_relative(cls, value: str | None) -> str | None:
|
|
82
|
+
return ensure_repository_relative(value)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class IssueRecord(StrictBaseModel):
|
|
86
|
+
"""Explicit issue record; lifecycle state is not used for every problem."""
|
|
87
|
+
|
|
88
|
+
issue_id: CanonicalId
|
|
89
|
+
kind: str
|
|
90
|
+
status: IssueStatus
|
|
91
|
+
description: str
|
|
92
|
+
impact: str
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class Identity(StrictBaseModel):
|
|
96
|
+
"""Checkpoint-specific card identity."""
|
|
97
|
+
|
|
98
|
+
model_name: str
|
|
99
|
+
model_family: str
|
|
100
|
+
variant: str
|
|
101
|
+
checkpoint_name: str
|
|
102
|
+
framework: Literal["pytorch"]
|
|
103
|
+
wrapper_entry_point: Annotated[
|
|
104
|
+
str,
|
|
105
|
+
Field(pattern=r"^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)*:[A-Z][A-Za-z0-9_]*$"),
|
|
106
|
+
]
|
|
107
|
+
|
|
108
|
+
@field_validator("wrapper_entry_point")
|
|
109
|
+
@classmethod
|
|
110
|
+
def validate_entry_point(cls, value: str) -> str:
|
|
111
|
+
return ensure_wrapper_entry_point(value)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class SourceRecord(StrictBaseModel):
|
|
115
|
+
"""Repository, package, or asset source evidence."""
|
|
116
|
+
|
|
117
|
+
source_id: CanonicalId
|
|
118
|
+
kind: str
|
|
119
|
+
url: HttpUrl | None = None
|
|
120
|
+
package: str | None = None
|
|
121
|
+
revision: Annotated[str | None, Field(pattern=GIT_REVISION_PATTERN)] = None
|
|
122
|
+
path: Annotated[str | None, Field(pattern=REPO_RELATIVE_PATTERN)] = None
|
|
123
|
+
evidence_status: EvidenceStatus
|
|
124
|
+
|
|
125
|
+
@field_validator("path")
|
|
126
|
+
@classmethod
|
|
127
|
+
def path_repository_relative(cls, value: str | None) -> str | None:
|
|
128
|
+
return ensure_repository_relative(value)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class Sources(StrictBaseModel):
|
|
132
|
+
"""Required source roles."""
|
|
133
|
+
|
|
134
|
+
official_repository: SourceRecord
|
|
135
|
+
implementation: SourceRecord
|
|
136
|
+
checkpoint: SourceRecord
|
|
137
|
+
wrapper: SourceRecord
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class ScientificReference(StrictBaseModel):
|
|
141
|
+
"""Publication metadata."""
|
|
142
|
+
|
|
143
|
+
title: str
|
|
144
|
+
doi: str | None = None
|
|
145
|
+
official_publication: HttpUrl | None = None
|
|
146
|
+
authors: tuple[str, ...]
|
|
147
|
+
year: int | None = Field(default=None, ge=1900, le=2100)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class Description(StrictBaseModel):
|
|
151
|
+
"""Separated descriptive claims."""
|
|
152
|
+
|
|
153
|
+
architecture: str
|
|
154
|
+
preprocessing: str
|
|
155
|
+
training_objective: str
|
|
156
|
+
checkpoint_behavior: str
|
|
157
|
+
implementation: str
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class Tasks(StrictBaseModel):
|
|
161
|
+
"""Task roles separated by phase."""
|
|
162
|
+
|
|
163
|
+
pretraining: tuple[str, ...]
|
|
164
|
+
finetuning: tuple[str, ...]
|
|
165
|
+
official_evaluation: tuple[str, ...]
|
|
166
|
+
supported_inference: tuple[str, ...]
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
class DatasetRecord(StrictBaseModel):
|
|
170
|
+
"""Dataset usage record."""
|
|
171
|
+
|
|
172
|
+
name: str
|
|
173
|
+
version: str | None = None
|
|
174
|
+
subset: str | None = None
|
|
175
|
+
split: str | None = None
|
|
176
|
+
role: str
|
|
177
|
+
source_status: EvidenceStatus
|
|
178
|
+
evidence_ids: tuple[str, ...] = ()
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class Datasets(StrictBaseModel):
|
|
182
|
+
"""Training, validation, and testing dataset partitions."""
|
|
183
|
+
|
|
184
|
+
training: tuple[DatasetRecord, ...]
|
|
185
|
+
validation: tuple[DatasetRecord, ...]
|
|
186
|
+
testing: tuple[DatasetRecord, ...]
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
class MetricRecord(StrictBaseModel):
|
|
190
|
+
"""Reported metric record."""
|
|
191
|
+
|
|
192
|
+
task: str
|
|
193
|
+
dataset: str
|
|
194
|
+
split: str
|
|
195
|
+
metric: str
|
|
196
|
+
value: float | str | None
|
|
197
|
+
unit: str | None = None
|
|
198
|
+
protocol: str
|
|
199
|
+
checkpoint_specific: bool
|
|
200
|
+
source_status: EvidenceStatus
|
|
201
|
+
evidence_ids: tuple[str, ...] = ()
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
class RecommendedEnvironment(StrictBaseModel):
|
|
205
|
+
"""Reference to committed environment artifacts."""
|
|
206
|
+
|
|
207
|
+
environment_id: CanonicalId
|
|
208
|
+
specification: Annotated[str, Field(pattern=REPO_RELATIVE_PATTERN)]
|
|
209
|
+
lockfile: Annotated[str, Field(pattern=REPO_RELATIVE_PATTERN)]
|
|
210
|
+
verified: bool
|
|
211
|
+
|
|
212
|
+
@field_validator("specification", "lockfile")
|
|
213
|
+
@classmethod
|
|
214
|
+
def paths_repository_relative(cls, value: str) -> str:
|
|
215
|
+
return ensure_repository_relative(value) or value
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
class Usage(StrictBaseModel):
|
|
219
|
+
"""Usage references and commands."""
|
|
220
|
+
|
|
221
|
+
recommended_environment: RecommendedEnvironment
|
|
222
|
+
installation_commands: tuple[str, ...]
|
|
223
|
+
checkpoint_loading: tuple[str, ...]
|
|
224
|
+
smoke_test_command: str
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
class WaveformInput(StrictBaseModel):
|
|
228
|
+
"""Canonical public waveform input contract."""
|
|
229
|
+
|
|
230
|
+
shape: Literal["B,C,T"]
|
|
231
|
+
sample_rate_hz: int = Field(gt=0)
|
|
232
|
+
dtype: str = "float32"
|
|
233
|
+
valid_lengths_shape: Literal["B"] | None = "B"
|
|
234
|
+
channels: str
|
|
235
|
+
resampling: str
|
|
236
|
+
padding: str
|
|
237
|
+
normalization: str
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
class OutputComponent(StrictBaseModel):
|
|
241
|
+
"""Stable output component description."""
|
|
242
|
+
|
|
243
|
+
name: str
|
|
244
|
+
kind: str
|
|
245
|
+
semantic_kind: str
|
|
246
|
+
rank: int = Field(ge=0)
|
|
247
|
+
layout: str
|
|
248
|
+
dimensions: tuple[str, ...]
|
|
249
|
+
dtype: str | None = None
|
|
250
|
+
granularity: str | None = None
|
|
251
|
+
task_head_relation: str | None = None
|
|
252
|
+
|
|
253
|
+
@model_validator(mode="after")
|
|
254
|
+
def rank_matches_dimensions(self) -> OutputComponent:
|
|
255
|
+
if len(self.dimensions) != self.rank:
|
|
256
|
+
raise ValueError("output component rank must match dimensions length")
|
|
257
|
+
return self
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
class Outputs(StrictBaseModel):
|
|
261
|
+
"""Forward and probability output declarations."""
|
|
262
|
+
|
|
263
|
+
primary: OutputComponent
|
|
264
|
+
components: tuple[OutputComponent, ...]
|
|
265
|
+
probability_output: OutputComponent | None = None
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
class EmbeddingsSection(StrictBaseModel):
|
|
269
|
+
"""Embedding declarations with one selected default."""
|
|
270
|
+
|
|
271
|
+
default_embedding_id: CanonicalId
|
|
272
|
+
items: tuple[EmbeddingSpec, ...]
|
|
273
|
+
|
|
274
|
+
@model_validator(mode="after")
|
|
275
|
+
def validate_default(self) -> EmbeddingsSection:
|
|
276
|
+
ids = [item.embedding_id for item in self.items]
|
|
277
|
+
if len(ids) != len(set(ids)):
|
|
278
|
+
raise ValueError("embedding IDs must be unique")
|
|
279
|
+
defaults = [item for item in self.items if item.default]
|
|
280
|
+
if len(defaults) != 1:
|
|
281
|
+
raise ValueError("exactly one embedding must be marked default")
|
|
282
|
+
if self.default_embedding_id != defaults[0].embedding_id:
|
|
283
|
+
raise ValueError("default_embedding_id must refer to the declared default embedding")
|
|
284
|
+
if self.default_embedding_id not in {item.embedding_id for item in self.items}:
|
|
285
|
+
raise ValueError("default_embedding_id must refer to a declared embedding")
|
|
286
|
+
return self
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
class BooleanCapability(StrictBaseModel):
|
|
290
|
+
"""Capability support plus optional reason."""
|
|
291
|
+
|
|
292
|
+
supported: bool
|
|
293
|
+
reason: str | None = None
|
|
294
|
+
|
|
295
|
+
@model_validator(mode="after")
|
|
296
|
+
def reason_for_unsupported(self) -> BooleanCapability:
|
|
297
|
+
if not self.supported and not self.reason:
|
|
298
|
+
raise ValueError("unsupported capabilities require a reason")
|
|
299
|
+
return self
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
class CapabilitiesSection(StrictBaseModel):
|
|
303
|
+
"""Model-independent capability declarations."""
|
|
304
|
+
|
|
305
|
+
random_initialization: BooleanCapability
|
|
306
|
+
checkpoint_loading: BooleanCapability
|
|
307
|
+
probabilities: BooleanCapability
|
|
308
|
+
embeddings: BooleanCapability
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
class DeviceSupport(StrictBaseModel):
|
|
312
|
+
"""Declared and observed device support."""
|
|
313
|
+
|
|
314
|
+
upstream_declared: tuple[str, ...]
|
|
315
|
+
locally_tested: tuple[str, ...]
|
|
316
|
+
known_limitations: tuple[str, ...]
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
class ProfilingSection(StrictBaseModel):
|
|
320
|
+
"""Profiling placeholder or completed profiling reference."""
|
|
321
|
+
|
|
322
|
+
status: ProfilingStatus
|
|
323
|
+
report: Annotated[str | None, Field(pattern=REPO_RELATIVE_PATTERN)] = None
|
|
324
|
+
|
|
325
|
+
@field_validator("report")
|
|
326
|
+
@classmethod
|
|
327
|
+
def report_repository_relative(cls, value: str | None) -> str | None:
|
|
328
|
+
return ensure_repository_relative(value)
|
|
329
|
+
|
|
330
|
+
@model_validator(mode="after")
|
|
331
|
+
def profiled_requires_report(self) -> ProfilingSection:
|
|
332
|
+
if self.status == ProfilingStatus.PROFILED and self.report is None:
|
|
333
|
+
raise ValueError("profiled sections require a report reference")
|
|
334
|
+
return self
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
class ModelCard(StrictBaseModel):
|
|
338
|
+
"""Validate the complete metadata for one checkpoint-specific integration.
|
|
339
|
+
|
|
340
|
+
A card represents exactly one model-family, variant, and checkpoint tuple. Validation enforces
|
|
341
|
+
lifecycle prerequisites, unique evidence identifiers, resolved evidence references, consistent
|
|
342
|
+
capabilities, repository-relative artifact paths, and the canonical waveform/output contracts.
|
|
343
|
+
|
|
344
|
+
Attributes
|
|
345
|
+
----------
|
|
346
|
+
card_id
|
|
347
|
+
Stable canonical identifier for this exact integration tuple.
|
|
348
|
+
card_status
|
|
349
|
+
Current lifecycle state. Later states require verified environment, checkpoint, runtime,
|
|
350
|
+
and profiling evidence as applicable.
|
|
351
|
+
identity
|
|
352
|
+
Model, variant, checkpoint, framework, and public wrapper identity.
|
|
353
|
+
input
|
|
354
|
+
Canonical waveform contract using ``[B,C,T]``, sample rate in hertz, and optional ``[B]``
|
|
355
|
+
valid lengths.
|
|
356
|
+
embeddings
|
|
357
|
+
Declared embedding tensors, including exactly one default when embeddings are supported.
|
|
358
|
+
evidence
|
|
359
|
+
Provenance records supporting material scientific and technical claims.
|
|
360
|
+
|
|
361
|
+
Raises
|
|
362
|
+
------
|
|
363
|
+
pydantic.ValidationError
|
|
364
|
+
If any field or cross-document lifecycle constraint is invalid.
|
|
365
|
+
|
|
366
|
+
Notes
|
|
367
|
+
-----
|
|
368
|
+
License fields are informational. Constructing a card never imports a model runtime, downloads
|
|
369
|
+
a checkpoint, or materializes an environment.
|
|
370
|
+
"""
|
|
371
|
+
|
|
372
|
+
schema_version: Literal["1.0.0"]
|
|
373
|
+
card_id: CanonicalId
|
|
374
|
+
card_status: ModelCardLifecycle
|
|
375
|
+
identity: Identity
|
|
376
|
+
checkpoint: CheckpointSpec
|
|
377
|
+
sources: Sources
|
|
378
|
+
scientific_reference: ScientificReference
|
|
379
|
+
description: Description
|
|
380
|
+
tasks: Tasks
|
|
381
|
+
datasets: Datasets
|
|
382
|
+
reported_metrics: tuple[MetricRecord, ...]
|
|
383
|
+
usage: Usage
|
|
384
|
+
input: WaveformInput
|
|
385
|
+
outputs: Outputs
|
|
386
|
+
embeddings: EmbeddingsSection
|
|
387
|
+
capabilities: CapabilitiesSection
|
|
388
|
+
device_support: DeviceSupport
|
|
389
|
+
verification_report: Annotated[str | None, Field(pattern=REPO_RELATIVE_PATTERN)] = None
|
|
390
|
+
architectural_profiling: ProfilingSection
|
|
391
|
+
inference_profiling: ProfilingSection
|
|
392
|
+
energy_profiling: ProfilingSection
|
|
393
|
+
limitations: tuple[str, ...]
|
|
394
|
+
issues: tuple[IssueRecord, ...]
|
|
395
|
+
evidence: tuple[EvidenceRecord, ...]
|
|
396
|
+
|
|
397
|
+
@field_validator("verification_report")
|
|
398
|
+
@classmethod
|
|
399
|
+
def verification_report_repository_relative(cls, value: str | None) -> str | None:
|
|
400
|
+
return ensure_repository_relative(value)
|
|
401
|
+
|
|
402
|
+
@model_validator(mode="after")
|
|
403
|
+
def validate_lifecycle(self) -> ModelCard:
|
|
404
|
+
if (
|
|
405
|
+
self.card_status
|
|
406
|
+
in {
|
|
407
|
+
ModelCardLifecycle.ENVIRONMENT_RESOLVED,
|
|
408
|
+
ModelCardLifecycle.CHECKPOINT_VERIFIED,
|
|
409
|
+
ModelCardLifecycle.RUNTIME_VERIFIED,
|
|
410
|
+
ModelCardLifecycle.PROFILED,
|
|
411
|
+
}
|
|
412
|
+
and not self.usage.recommended_environment.verified
|
|
413
|
+
):
|
|
414
|
+
raise ValueError("environment_resolved or later cards require verified environment")
|
|
415
|
+
if (
|
|
416
|
+
self.card_status
|
|
417
|
+
in {
|
|
418
|
+
ModelCardLifecycle.CHECKPOINT_VERIFIED,
|
|
419
|
+
ModelCardLifecycle.RUNTIME_VERIFIED,
|
|
420
|
+
ModelCardLifecycle.PROFILED,
|
|
421
|
+
}
|
|
422
|
+
and self.checkpoint.observed_sha256 is None
|
|
423
|
+
):
|
|
424
|
+
raise ValueError(
|
|
425
|
+
"checkpoint_verified or later cards require observed checkpoint SHA-256"
|
|
426
|
+
)
|
|
427
|
+
if (
|
|
428
|
+
self.card_status
|
|
429
|
+
in {
|
|
430
|
+
ModelCardLifecycle.RUNTIME_VERIFIED,
|
|
431
|
+
ModelCardLifecycle.PROFILED,
|
|
432
|
+
}
|
|
433
|
+
and self.verification_report is None
|
|
434
|
+
):
|
|
435
|
+
raise ValueError("runtime_verified and profiled cards require verification_report")
|
|
436
|
+
if self.card_status == ModelCardLifecycle.PROFILED:
|
|
437
|
+
for section in (
|
|
438
|
+
self.architectural_profiling,
|
|
439
|
+
self.inference_profiling,
|
|
440
|
+
self.energy_profiling,
|
|
441
|
+
):
|
|
442
|
+
if section.status != ProfilingStatus.PROFILED:
|
|
443
|
+
raise ValueError(
|
|
444
|
+
"profiled cards require all profiling sections marked profiled"
|
|
445
|
+
)
|
|
446
|
+
return self
|
|
447
|
+
|
|
448
|
+
@model_validator(mode="after")
|
|
449
|
+
def validate_capabilities_and_evidence(self) -> ModelCard:
|
|
450
|
+
if self.capabilities.probabilities.supported != (
|
|
451
|
+
self.outputs.probability_output is not None
|
|
452
|
+
):
|
|
453
|
+
raise ValueError("probability capability must agree with probability output")
|
|
454
|
+
if self.capabilities.embeddings.supported != bool(self.embeddings.items):
|
|
455
|
+
raise ValueError("embedding capability must agree with embedding declarations")
|
|
456
|
+
|
|
457
|
+
evidence_ids = [item.evidence_id for item in self.evidence]
|
|
458
|
+
if len(evidence_ids) != len(set(evidence_ids)):
|
|
459
|
+
raise ValueError("evidence IDs must be unique")
|
|
460
|
+
declared = set(evidence_ids)
|
|
461
|
+
references: list[str] = []
|
|
462
|
+
for dataset in self.datasets.training + self.datasets.validation + self.datasets.testing:
|
|
463
|
+
references.extend(dataset.evidence_ids)
|
|
464
|
+
for metric in self.reported_metrics:
|
|
465
|
+
references.extend(metric.evidence_ids)
|
|
466
|
+
for embedding in self.embeddings.items:
|
|
467
|
+
references.extend(embedding.evidence_ids)
|
|
468
|
+
missing = sorted(set(references) - declared)
|
|
469
|
+
if missing:
|
|
470
|
+
raise ValueError(f"evidence references are unresolved: {missing}")
|
|
471
|
+
return self
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Dual Pydantic and JSON Schema validation for model cards."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from jsonschema import Draft202012Validator, FormatChecker
|
|
10
|
+
from jsonschema.exceptions import ValidationError as JsonSchemaValidationError
|
|
11
|
+
from pydantic import ValidationError as PydanticValidationError
|
|
12
|
+
|
|
13
|
+
from torch_dae.cards.models import ModelCard
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def load_json(path: Path) -> dict[str, Any]:
|
|
17
|
+
"""Load a JSON object from disk."""
|
|
18
|
+
|
|
19
|
+
value = json.loads(path.read_text())
|
|
20
|
+
if not isinstance(value, dict):
|
|
21
|
+
raise ValueError(f"{path} did not contain a JSON object")
|
|
22
|
+
return value
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def validate_model_card_pydantic(data: dict[str, Any]) -> ModelCard:
|
|
26
|
+
"""Validate a card through Pydantic."""
|
|
27
|
+
|
|
28
|
+
return ModelCard.model_validate(data)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def validate_model_card_schema(data: dict[str, Any], schema_path: Path) -> None:
|
|
32
|
+
"""Validate a card through JSON Schema with format checking."""
|
|
33
|
+
|
|
34
|
+
schema = load_json(schema_path)
|
|
35
|
+
Draft202012Validator(schema, format_checker=FormatChecker()).validate(data)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def assert_valid_pydantic(data: dict[str, Any]) -> ModelCard:
|
|
39
|
+
"""Assert that Pydantic accepts a model card."""
|
|
40
|
+
|
|
41
|
+
return validate_model_card_pydantic(data)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def assert_invalid_pydantic(data: dict[str, Any]) -> None:
|
|
45
|
+
"""Assert that Pydantic rejects a model card."""
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
validate_model_card_pydantic(data)
|
|
49
|
+
except PydanticValidationError:
|
|
50
|
+
return
|
|
51
|
+
raise AssertionError("expected Pydantic validation to fail")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def assert_valid_json_schema(data: dict[str, Any], schema_path: Path) -> None:
|
|
55
|
+
"""Assert that JSON Schema accepts a model card."""
|
|
56
|
+
|
|
57
|
+
validate_model_card_schema(data, schema_path)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def assert_invalid_json_schema(data: dict[str, Any], schema_path: Path) -> None:
|
|
61
|
+
"""Assert that JSON Schema rejects a model card."""
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
validate_model_card_schema(data, schema_path)
|
|
65
|
+
except JsonSchemaValidationError:
|
|
66
|
+
return
|
|
67
|
+
raise AssertionError("expected JSON Schema validation to fail")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def validate_model_card_path(path: Path, schema_path: Path | None = None) -> ModelCard:
|
|
71
|
+
"""Validate a model-card file through both validators when a schema is available."""
|
|
72
|
+
|
|
73
|
+
data = load_json(path)
|
|
74
|
+
card = validate_model_card_pydantic(data)
|
|
75
|
+
if schema_path is not None:
|
|
76
|
+
validate_model_card_schema(data, schema_path)
|
|
77
|
+
return card
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""CLI package."""
|
torch_dae/cli/cards.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Model-card CLI commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
from torch_dae.cards.validation import validate_model_card_path
|
|
10
|
+
from torch_dae.core.registry import ModelCardRegistry
|
|
11
|
+
from torch_dae.environment.manager import discover_repository_root
|
|
12
|
+
|
|
13
|
+
app = typer.Typer(no_args_is_help=True, help="Model-card commands.")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@app.command("list")
|
|
17
|
+
def list_cards() -> None:
|
|
18
|
+
"""List discovered model cards."""
|
|
19
|
+
|
|
20
|
+
registry = ModelCardRegistry(discover_repository_root())
|
|
21
|
+
for card in registry.list_cards():
|
|
22
|
+
typer.echo(card.card_id)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@app.command("show")
|
|
26
|
+
def show(card_id: str) -> None:
|
|
27
|
+
"""Show a model card as normalized JSON."""
|
|
28
|
+
|
|
29
|
+
registry = ModelCardRegistry(discover_repository_root())
|
|
30
|
+
typer.echo(registry.get_card(card_id).model_dump_json(indent=2))
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@app.command("validate")
|
|
34
|
+
def validate(card_id_or_path: str) -> None:
|
|
35
|
+
"""Validate a card id or JSON path."""
|
|
36
|
+
|
|
37
|
+
root = discover_repository_root()
|
|
38
|
+
candidate = Path(card_id_or_path)
|
|
39
|
+
if candidate.exists():
|
|
40
|
+
path = candidate
|
|
41
|
+
else:
|
|
42
|
+
registry = ModelCardRegistry(root)
|
|
43
|
+
path = registry.get_card_path(card_id_or_path)
|
|
44
|
+
validate_model_card_path(path, root / "schemas/model-card.schema.json")
|
|
45
|
+
typer.echo(f"valid: {path}")
|