Coverage for src/bioimageio/spec/_hf_card.py: 81%

303 statements  

« prev     ^ index     » next       coverage.py v7.14.1, created at 2026-06-15 15:08 +0000

1import collections.abc 

2import warnings 

3from functools import partial 

4from pathlib import PurePosixPath 

5from typing import Any, Dict, List, Optional, Sequence, Tuple, Union 

6 

7import numpy as np 

8from imageio.v3 import imwrite # pyright: ignore[reportUnknownVariableType] 

9from loguru import logger 

10from numpy.typing import NDArray 

11from typing_extensions import assert_never 

12 

13from bioimageio.spec._internal.validation_context import get_validation_context 

14from bioimageio.spec.model.v0_5 import ( 

15 FileDescr, 

16 IntervalOrRatioDataDescr, 

17 KerasHdf5WeightsDescr, 

18 KerasV3WeightsDescr, 

19 NominalOrOrdinalDataDescr, 

20 OnnxWeightsDescr, 

21 PytorchStateDictWeightsDescr, 

22 TensorflowJsWeightsDescr, 

23 TensorflowSavedModelBundleWeightsDescr, 

24 TensorId, 

25 TorchscriptWeightsDescr, 

26) 

27 

28from ._internal.io import RelativeFilePath, get_reader 

29from ._internal.io_utils import load_array 

30from ._version import VERSION 

31from .model import ModelDescr 

32from .utils import get_spdx_licenses, load_image 

33 

34HF_KNOWN_LICENSES = ( 

35 "apache-2.0", 

36 "mit", 

37 "openrail", 

38 "bigscience-openrail-m", 

39 "creativeml-openrail-m", 

40 "bigscience-bloom-rail-1.0", 

41 "bigcode-openrail-m", 

42 "afl-3.0", 

43 "artistic-2.0", 

44 "bsl-1.0", 

45 "bsd", 

46 "bsd-2-clause", 

47 "bsd-3-clause", 

48 "bsd-3-clause-clear", 

49 "c-uda", 

50 "cc", 

51 "cc0-1.0", 

52 "cc-by-2.0", 

53 "cc-by-2.5", 

54 "cc-by-3.0", 

55 "cc-by-4.0", 

56 "cc-by-sa-3.0", 

57 "cc-by-sa-4.0", 

58 "cc-by-nc-2.0", 

59 "cc-by-nc-3.0", 

60 "cc-by-nc-4.0", 

61 "cc-by-nd-4.0", 

62 "cc-by-nc-nd-3.0", 

63 "cc-by-nc-nd-4.0", 

64 "cc-by-nc-sa-2.0", 

65 "cc-by-nc-sa-3.0", 

66 "cc-by-nc-sa-4.0", 

67 "cdla-sharing-1.0", 

68 "cdla-permissive-1.0", 

69 "cdla-permissive-2.0", 

70 "wtfpl", 

71 "ecl-2.0", 

72 "epl-1.0", 

73 "epl-2.0", 

74 "etalab-2.0", 

75 "eupl-1.1", 

76 "eupl-1.2", 

77 "agpl-3.0", 

78 "gfdl", 

79 "gpl", 

80 "gpl-2.0", 

81 "gpl-3.0", 

82 "lgpl", 

83 "lgpl-2.1", 

84 "lgpl-3.0", 

85 "isc", 

86 "h-research", 

87 "intel-research", 

88 "lppl-1.3c", 

89 "ms-pl", 

90 "apple-ascl", 

91 "apple-amlr", 

92 "mpl-2.0", 

93 "odc-by", 

94 "odbl", 

95 "openmdw-1.0", 

96 "openrail++", 

97 "osl-3.0", 

98 "postgresql", 

99 "ofl-1.1", 

100 "ncsa", 

101 "unlicense", 

102 "zlib", 

103 "pddl", 

104 "lgpl-lr", 

105 "deepfloyd-if-license", 

106 "fair-noncommercial-research-license", 

107 "llama2", 

108 "llama3", 

109 "llama3.1", 

110 "llama3.2", 

111 "llama3.3", 

112 "llama4", 

113 "grok2-community", 

114 "gemma", 

115) 

116 

117 

118def _generate_png_from_tensor(tensor: NDArray[np.generic]) -> Optional[bytes]: 

119 """Generate PNG bytes from a sample tensor. 

120 

121 Prefers 2D slices from multi-dimensional arrays. 

122 Returns PNG bytes or None if generation fails. 

123 """ 

124 try: 

125 # Squeeze out singleton dimensions 

126 arr = np.squeeze(tensor) 

127 

128 # Handle different dimensionalities 

129 if arr.ndim == 2: 

130 img_data = arr 

131 elif arr.ndim == 3: 

132 # Could be (H, W, C) or (Z, H, W) 

133 if arr.shape[-1] in [1, 3, 4]: # Likely channels last 

134 img_data = arr 

135 else: # Take middle slice 

136 img_data = arr[arr.shape[0] // 2] 

137 elif arr.ndim == 4: 

138 # Take middle slices (e.g., batch, z, y, x) 

139 img_data = ( 

140 arr[0, arr.shape[1] // 2] 

141 if arr.shape[0] == 1 

142 else arr[arr.shape[0] // 2, arr.shape[1] // 2] 

143 ) 

144 elif arr.ndim > 4: 

145 # Take middle slices of all extra dimensions 

146 slices = tuple(s // 2 for s in arr.shape[:-2]) 

147 img_data = arr[slices] 

148 else: 

149 return None 

150 

151 # Normalize to 0-255 uint8 

152 img_data = np.squeeze(img_data) 

153 if img_data.dtype != np.uint8: 

154 img_min, img_max = img_data.min(), img_data.max() 

155 if img_max > img_min: 

156 img_data: NDArray[Any] = (img_data - img_min) / (img_max - img_min) 

157 else: 

158 img_data = np.zeros_like(img_data) 

159 img_data = (img_data * 255).astype(np.uint8) 

160 return imwrite("<bytes>", img_data, extension=".png") 

161 except Exception: 

162 return None 

163 

164 

165def _get_io_description( 

166 model: ModelDescr, 

167) -> Tuple[str, Dict[str, bytes], List[TensorId], List[TensorId]]: 

168 """Generate a description of model inputs and outputs with sample images. 

169 

170 Returns: 

171 A tuple of (markdown_string, referenced_files_dict, input_ids, output_ids) where referenced_files_dict maps 

172 filenames to file bytes. 

173 """ 

174 markdown_string = "" 

175 referenced_files: dict[str, bytes] = {} 

176 input_ids: List[TensorId] = [] 

177 output_ids: List[TensorId] = [] 

178 

179 def format_data_descr( 

180 d: Union[ 

181 NominalOrOrdinalDataDescr, 

182 IntervalOrRatioDataDescr, 

183 Sequence[Union[NominalOrOrdinalDataDescr, IntervalOrRatioDataDescr]], 

184 ], 

185 ) -> str: 

186 ret = "" 

187 if isinstance(d, NominalOrOrdinalDataDescr): 

188 ret += f" - Values: {d.values}\n" 

189 elif isinstance(d, IntervalOrRatioDataDescr): 

190 ret += f" - Value unit: {d.unit}\n" 

191 ret += f" - Value scale factor: {d.scale}\n" 

192 if d.offset is not None: 

193 ret += f" - Value offset: {d.offset}\n" 

194 elif d.range[0] is not None: 

195 ret += f" - Value minimum: {d.range[0]}\n" 

196 elif d.range[1] is not None: 

197 ret += f" - Value maximum: {d.range[1]}\n" 

198 elif isinstance(d, collections.abc.Sequence): 

199 for dd in d: 

200 ret += format_data_descr(dd) 

201 else: 

202 assert_never(d) 

203 

204 return ret 

205 

206 # Input descriptions 

207 if model.inputs: 

208 markdown_string += "\n- **Input specifications:**\n" 

209 

210 for inp in model.inputs: 

211 input_ids.append(inp.id) 

212 axes_str = ", ".join(str(a.id) for a in inp.axes) 

213 shape_str = " × ".join( 

214 str(a.size) if isinstance(a.size, int) else str(a.size) 

215 for a in inp.axes 

216 ) 

217 

218 markdown_string += f" `{inp.id}`: {inp.description or ''}\n\n" 

219 markdown_string += f" - Axes: `{axes_str}`\n" 

220 markdown_string += f" - Shape: `{shape_str}`\n" 

221 markdown_string += f" - Data type: `{inp.dtype}`\n" 

222 markdown_string += format_data_descr(inp.data) 

223 

224 # Try to load and display sample_tensor (preferred) or test_tensor 

225 img_bytes = None 

226 if inp.sample_tensor is not None: 

227 try: 

228 arr = load_image(inp.sample_tensor) 

229 img_bytes = _generate_png_from_tensor(arr) 

230 except Exception as e: 

231 logger.error("failed to generate input sample image: {}", e) 

232 

233 if img_bytes is None and inp.test_tensor is not None: 

234 try: 

235 arr = load_array(inp.test_tensor) 

236 img_bytes = _generate_png_from_tensor(arr) 

237 except Exception as e: 

238 logger.error( 

239 "failed to generate input sample image from test data: {}", e 

240 ) 

241 

242 if img_bytes: 

243 filename = f"images/input_{inp.id}_sample.png" 

244 referenced_files[filename] = img_bytes 

245 markdown_string += f" - example\n ![{inp.id} sample]({filename})\n" 

246 

247 # Output descriptions 

248 if model.outputs: 

249 markdown_string += "\n- **Output specifications:**\n" 

250 for out in model.outputs: 

251 output_ids.append(out.id) 

252 axes_str = ", ".join(str(a.id) for a in out.axes) 

253 shape_str = " × ".join( 

254 str(a.size) if isinstance(a.size, int) else str(a.size) 

255 for a in out.axes 

256 ) 

257 

258 markdown_string += f" `{out.id}`: {out.description or ''}\n" 

259 markdown_string += f" - Axes: `{axes_str}`\n" 

260 markdown_string += f" - Shape: `{shape_str}`\n" 

261 markdown_string += f" - Data type: `{out.dtype}`\n" 

262 markdown_string += format_data_descr(out.data) 

263 

264 # Try to load and display sample_tensor (preferred) or test_tensor 

265 img_bytes = None 

266 if out.sample_tensor is not None: 

267 try: 

268 arr = load_image(out.sample_tensor) 

269 img_bytes = _generate_png_from_tensor(arr) 

270 except Exception as e: 

271 logger.error("failed to generate output sample image: {}", e) 

272 

273 if img_bytes is None and out.test_tensor is not None: 

274 try: 

275 arr = load_array(out.test_tensor) 

276 img_bytes = _generate_png_from_tensor(arr) 

277 except Exception as e: 

278 logger.error( 

279 "failed to generate output sample image from test data: {}", e 

280 ) 

281 

282 if img_bytes: 

283 filename = f"images/output_{out.id}_sample.png" 

284 referenced_files[filename] = img_bytes 

285 markdown_string += f" - example\n {out.id} sample]({filename})\n" 

286 

287 return markdown_string, referenced_files, input_ids, output_ids 

288 

289 

290def create_huggingface_model_card( 

291 model: ModelDescr, *, repo_id: str 

292) -> Tuple[str, Dict[str, bytes]]: 

293 """Create a Hugging Face model card for a BioImage.IO model. 

294 

295 Returns: 

296 A tuple of (markdown_string, images_dict) where images_dict maps 

297 filenames to PNG bytes that should be saved alongside the markdown. 

298 """ 

299 model = model.model_copy() 

300 

301 if model.version is None: 

302 model_version = "" 

303 else: 

304 model_version = f"\n- **model version:** {model.version}" 

305 

306 if model.documentation is None: 

307 additional_model_doc = "" 

308 else: 

309 doc_reader = get_reader(model.documentation) 

310 local_doc_path = f"package/{doc_reader.original_file_name}" 

311 with get_validation_context().replace(perform_io_checks=False): 

312 model.documentation = FileDescr( 

313 source=RelativeFilePath(PurePosixPath(local_doc_path)) 

314 ) 

315 

316 additional_model_doc = f"\n- **Additional model documentation:** [{local_doc_path}]({local_doc_path})" 

317 

318 if model.cite: 

319 developed_by = "\n- **Developed by:** " + ( 

320 "".join( 

321 ( 

322 f"\n - {c.text}: " 

323 + (f"https://www.doi.org/{c.doi}" if c.doi else str(c.url)) 

324 ) 

325 for c in model.cite 

326 ) 

327 ) 

328 else: 

329 developed_by = "" 

330 

331 if model.config.bioimageio.funded_by: 

332 funded_by = f"\n- **Funded by:** {model.config.bioimageio.funded_by}" 

333 else: 

334 funded_by = "" 

335 

336 if model.authors: 

337 shared_by = "\n- **Shared by:** " + ( 

338 "".join( 

339 ( 

340 f"\n - {a.name}" 

341 + (f", {a.affiliation}" if a.affiliation else "") 

342 + ( 

343 f", [https://orcid.org/{a.orcid}](https://orcid.org/{a.orcid})" 

344 if a.orcid 

345 else "" 

346 ) 

347 + ( 

348 f", [https://github.com/{a.github_user}](https://github.com/{a.github_user})" 

349 if a.github_user 

350 else "" 

351 ) 

352 for a in model.authors 

353 ) 

354 ) 

355 ) 

356 else: 

357 shared_by = "" 

358 

359 if model.config.bioimageio.architecture_type: 

360 model_type = f"\n- **Model type:** {model.config.bioimageio.architecture_type}" 

361 else: 

362 model_type = "" 

363 

364 if model.config.bioimageio.modality: 

365 model_modality = f"\n- **Modality:** {model.config.bioimageio.modality}" 

366 else: 

367 model_modality = "" 

368 

369 if model.config.bioimageio.target_structure: 

370 target_structures = "\n- **Target structures:** " + ", ".join( 

371 model.config.bioimageio.target_structure 

372 ) 

373 else: 

374 target_structures = "" 

375 

376 if model.config.bioimageio.task: 

377 task_type = f"\n- **Task type:** {model.config.bioimageio.task}" 

378 else: 

379 task_type = "" 

380 

381 if model.parent: 

382 finetuned_from = f"\n- **Finetuned from model:** {model.parent.id}" 

383 else: 

384 finetuned_from = "" 

385 

386 repository = ( 

387 f"[{model.git_repo}]({model.git_repo})" if model.git_repo else "missing" 

388 ) 

389 

390 dl_framework_parts: List[str] = [] 

391 training_frameworks: List[str] = [] 

392 model_size: Optional[str] = None 

393 for weights in model.weights.available_formats.values(): 

394 if isinstance(weights, (PytorchStateDictWeightsDescr, TorchscriptWeightsDescr)): 

395 dl_framework_version = weights.pytorch_version 

396 elif isinstance( 

397 weights, 

398 ( 

399 TensorflowSavedModelBundleWeightsDescr, 

400 TensorflowJsWeightsDescr, 

401 KerasHdf5WeightsDescr, 

402 ), 

403 ): 

404 dl_framework_version = weights.tensorflow_version 

405 elif isinstance(weights, KerasV3WeightsDescr): 

406 dl_framework_version = weights.keras_version 

407 elif isinstance(weights, OnnxWeightsDescr): 

408 dl_framework_version = f"opset version: {weights.opset_version}" 

409 else: 

410 assert_never(weights) 

411 

412 if weights.parent is None: 

413 training_frameworks.append(weights.weights_format_name) 

414 

415 dl_framework_parts.append( 

416 f"\n - {weights.weights_format_name}: {dl_framework_version}" 

417 ) 

418 

419 if model_size is None: 

420 s = 0 

421 r = weights.get_reader() 

422 for chunk in iter(partial(r.read, 128 * 1024), b""): 

423 s += len(chunk) 

424 

425 if model.config.bioimageio.model_parameter_count is not None: 

426 if model.config.bioimageio.model_parameter_count < 1e9: 

427 model_size = f"{model.config.bioimageio.model_parameter_count / 1e6:.2f} million parameters, " 

428 else: 

429 model_size = f"{model.config.bioimageio.model_parameter_count / 1e9:.2f} billion parameters, " 

430 else: 

431 model_size = "" 

432 

433 if s < 1e9: 

434 model_size += f"{s / 1e6:.2f} MB" 

435 else: 

436 model_size += f"{s / 1e9:.2f} GB" 

437 

438 dl_frameworks = "".join(dl_framework_parts) 

439 if len(training_frameworks) > 1: 

440 warnings.warn( 

441 "Multiple training frameworks detected. (Some weight formats are probably missing a `parent` reference.)" 

442 ) 

443 

444 if ( 

445 model.weights.pytorch_state_dict is not None 

446 and model.weights.pytorch_state_dict.dependencies is not None 

447 ): 

448 env_reader = model.weights.pytorch_state_dict.dependencies.get_reader() 

449 dependencies = f"Dependencies for Pytorch State dict weights are listed in [{env_reader.original_file_name}](package/{env_reader.original_file_name})." 

450 else: 

451 dependencies = "None beyond the respective framework library." 

452 

453 out_of_scope_use = ( 

454 model.config.bioimageio.out_of_scope_use 

455 if model.config.bioimageio.out_of_scope_use 

456 else """missing; therefore these typical limitations should be considered: 

457 

458- *Likely not suitable for diagnostic purposes.* 

459- *Likely not validated for different imaging modalities than present in the training data.* 

460- *Should not be used without proper validation on user's specific datasets.* 

461 

462""" 

463 ) 

464 

465 environmental_impact = model.config.bioimageio.environmental_impact.format_md() 

466 if environmental_impact: 

467 environmental_impact_toc_entry = ( 

468 "\n- [Environmental Impact](#environmental-impact)" 

469 ) 

470 else: 

471 environmental_impact_toc_entry = "" 

472 

473 evaluation_parts: List[str] = [] 

474 n_evals = 0 

475 for e in model.config.bioimageio.evaluations: 

476 if e.dataset_role == "independent": 

477 continue # treated separately below 

478 

479 n_evals += 1 

480 n_evals_str = "" if n_evals == 1 else f" {n_evals}" 

481 evaluation_parts.append(f"\n# Evaluation{n_evals_str}\n") 

482 evaluation_parts.append(e.format_md()) 

483 

484 n_evals = 0 

485 for e in model.config.bioimageio.evaluations: 

486 if e.dataset_role != "independent": 

487 continue # treated separately above 

488 

489 n_evals += 1 

490 n_evals_str = "" if n_evals == 1 else f" {n_evals}" 

491 

492 evaluation_parts.append(f"### Validation on External Data{n_evals_str}\n") 

493 evaluation_parts.append(e.format_md()) 

494 

495 if evaluation_parts: 

496 evaluation = "\n".join(evaluation_parts) 

497 evaluation_toc_entry = "\n- [Evaluation](#evaluation)" 

498 else: 

499 evaluation = "" 

500 evaluation_toc_entry = "" 

501 

502 training_details = "" 

503 if model.config.bioimageio.training.training_preprocessing: 

504 training_details += f"### Preprocessing\n\n{model.config.bioimageio.training.training_preprocessing}\n\n" 

505 

506 training_details += "### Training Hyperparameters\n\n" 

507 training_details += f"- **Framework:** {' / '.join(training_frameworks)}" 

508 if model.config.bioimageio.training.training_epochs is not None: 

509 training_details += ( 

510 f"- **Epochs:** {model.config.bioimageio.training.training_epochs}\n" 

511 ) 

512 

513 if model.config.bioimageio.training.training_batch_size is not None: 

514 training_details += f"- **Batch size:** {model.config.bioimageio.training.training_batch_size}\n" 

515 

516 if model.config.bioimageio.training.initial_learning_rate is not None: 

517 training_details += f"- **Initial learning rate:** {model.config.bioimageio.training.initial_learning_rate}\n" 

518 

519 if model.config.bioimageio.training.learning_rate_schedule is not None: 

520 training_details += f"- **Learning rate schedule:** {model.config.bioimageio.training.learning_rate_schedule}\n" 

521 

522 if model.config.bioimageio.training.loss_function is not None: 

523 training_details += ( 

524 f"- **Loss function:** {model.config.bioimageio.training.loss_function}" 

525 ) 

526 if model.config.bioimageio.training.loss_function_kwargs: 

527 training_details += ( 

528 f" with {model.config.bioimageio.training.loss_function_kwargs}" 

529 ) 

530 training_details += "\n" 

531 

532 if model.config.bioimageio.training.optimizer is not None: 

533 training_details += ( 

534 f"- **Optimizer:** {model.config.bioimageio.training.optimizer}" 

535 ) 

536 if model.config.bioimageio.training.optimizer_kwargs: 

537 training_details += ( 

538 f" with {model.config.bioimageio.training.optimizer_kwargs}" 

539 ) 

540 training_details += "\n" 

541 

542 if model.config.bioimageio.training.regularization is not None: 

543 training_details += ( 

544 f"- **Regularization:** {model.config.bioimageio.training.regularization}\n" 

545 ) 

546 

547 speeds_sizes_times = "### Speeds, Sizes, Times\n\n" 

548 if model.config.bioimageio.training.training_duration is not None: 

549 speeds_sizes_times += f"- **Training time:** {'{:.2f}'.format(model.config.bioimageio.training.training_duration)}\n" 

550 

551 speeds_sizes_times += f"- **Model size:** {model_size}\n" 

552 if model.config.bioimageio.inference_time: 

553 speeds_sizes_times += ( 

554 f"- **Inference time:** {model.config.bioimageio.inference_time}\n" 

555 ) 

556 

557 if model.config.bioimageio.memory_requirements_inference: 

558 speeds_sizes_times += f"- **Memory requirements:** {model.config.bioimageio.memory_requirements_inference}\n" 

559 

560 model_arch_and_objective = "## Model Architecture and Objective\n\n" 

561 if ( 

562 model.config.bioimageio.architecture_type 

563 or model.config.bioimageio.architecture_description 

564 ): 

565 model_arch_and_objective += ( 

566 f"- **Architecture:** {model.config.bioimageio.architecture_type or ''}" 

567 + ( 

568 " --- " 

569 if model.config.bioimageio.architecture_type 

570 and model.config.bioimageio.architecture_description 

571 else "" 

572 ) 

573 + ( 

574 model.config.bioimageio.architecture_description 

575 if model.config.bioimageio.architecture_description is not None 

576 else "" 

577 ) 

578 + "\n" 

579 ) 

580 

581 io_desc, referenced_files, input_ids, output_ids = _get_io_description(model) 

582 predict_snippet_inputs = str( 

583 {input_id: "<path or tensor>" for input_id in input_ids} 

584 ) 

585 model_arch_and_objective += io_desc 

586 

587 hardware_requirements = "\n### Hardware Requirements\n" 

588 if model.config.bioimageio.memory_requirements_training is not None: 

589 hardware_requirements += f"- **Training:** GPU memory: {model.config.bioimageio.memory_requirements_training}\n" 

590 

591 if model.config.bioimageio.memory_requirements_inference is not None: 

592 hardware_requirements += f"- **Inference:** GPU memory: {model.config.bioimageio.memory_requirements_inference}\n" 

593 

594 hardware_requirements += f"- **Storage:** Model size: {model_size}\n" 

595 

596 if model.license is None: 

597 license = "unknown" 

598 license_meta = "unknown" 

599 elif isinstance(model.license, FileDescr): 

600 license_reader = get_reader(model.license) 

601 local_license_path = f"package/{license_reader.original_file_name}" 

602 with get_validation_context().replace(perform_io_checks=False): 

603 model.license.source = RelativeFilePath(PurePosixPath(local_license_path)) 

604 

605 license = f"[{local_license_path}]({local_license_path})" 

606 license_meta = "unknown" 

607 else: 

608 spdx_licenses = get_spdx_licenses() 

609 matches = [ 

610 (entry["name"], entry["reference"]) 

611 for entry in spdx_licenses["licenses"] 

612 if entry["licenseId"].lower() == model.license.lower() 

613 ] 

614 if matches: 

615 if len(matches) > 1: 

616 logger.warning( 

617 "Multiple SPDX license matches found for '{}', using the first one.", 

618 model.license, 

619 ) 

620 name, reference = matches[0] 

621 license = f"[{name}]({reference})" 

622 if model.license.lower() in HF_KNOWN_LICENSES: 

623 license_meta = model.license.lower() 

624 else: 

625 license_meta = f"other\nlicense_name: {model.license.lower()}\nlicense_link: {reference}" 

626 else: 

627 if model.license.lower() in HF_KNOWN_LICENSES: 

628 license_meta = model.license.lower() 

629 else: 

630 license_meta = "unknown" 

631 

632 license = model.license.lower() 

633 

634 base_model = ( 

635 f"\nbase_model: {model.parent.id[len('huggingface/') :]}" 

636 if model.parent is not None and model.parent.id.startswith("huggingface/") 

637 else "" 

638 ) 

639 dataset_meta = ( 

640 f"\ndataset: {model.training_data.id[len('huggingface/') :]}" 

641 if model.training_data is not None 

642 and model.training_data.id is not None 

643 and model.training_data.id.startswith("huggingface/") 

644 else "" 

645 ) 

646 if model.covers: 

647 cover_image_reader = get_reader(model.covers[0]) 

648 cover_image_bytes = cover_image_reader.read() 

649 cover_image_filename = f"images/{cover_image_reader.original_file_name}" 

650 referenced_files[cover_image_filename] = cover_image_bytes 

651 cover_image_md = f"\n![cover image]({cover_image_filename})\n\n" 

652 thumbnail_meta = ( 

653 f"\nthumbnail: {cover_image_filename}" # TODO: fix this to be a proper URL 

654 ) 

655 

656 else: 

657 cover_image_md = "" 

658 thumbnail_meta = "" 

659 

660 # TODO: add pipeline_tag to metadata 

661 readme = f"""--- 

662license: {license_meta}{thumbnail_meta} 

663tags: {list({"biology"}.union(set(model.tags)))} 

664language: [en] 

665library_name: bioimageio{base_model}{dataset_meta} 

666--- 

667# {model.name}{cover_image_md} 

668 

669{model.description or ""} 

670 

671 

672# Table of Contents 

673 

674- [Model Details](#model-details) 

675- [Uses](#uses) 

676- [Bias, Risks, and Limitations](#bias-risks-and-limitations) 

677- [How to Get Started with the Model](#how-to-get-started-with-the-model) 

678- [Training Details](#training-details){evaluation_toc_entry}{ 

679 environmental_impact_toc_entry 

680 } 

681- [Technical Specifications](#technical-specifications) 

682 

683 

684# Model Details 

685 

686## Model Description 

687{model_version}{additional_model_doc}{developed_by}{funded_by}{shared_by}{model_type}{ 

688 model_modality 

689 }{target_structures}{task_type} 

690- **License:** {license}{finetuned_from} 

691 

692## Model Sources 

693 

694- **Repository:** {repository} 

695- **Paper:** see [**Developed by**](#model-description) 

696 

697# Uses 

698 

699## Direct Use 

700 

701This model is compatible with the bioimageio.spec Python package (version >= { 

702 VERSION 

703 }) and the bioimageio.core Python package supporting model inference in Python code or via the `bioimageio` CLI. 

704 

705```python 

706from bioimageio.core import predict 

707 

708output_sample = predict( 

709 "huggingface/{repo_id}/{model.version or "draft"}", 

710 inputs={predict_snippet_inputs}, 

711) 

712 

713output_tensor = output_sample.members["{ 

714 output_ids[0] if output_ids else "<output_id>" 

715 }"] 

716xarray_dataarray = output_tensor.data 

717numpy_ndarray = output_tensor.data.to_numpy() 

718``` 

719 

720## Downstream Use 

721 

722Specific bioimage.io partner tool compatibilities may be reported at [Compatibility Reports](https://bioimage-io.github.io/collection/latest/compatibility/#compatibility-by-resource). 

723{ 

724 "Training (and fine-tuning) code may be available at " + model.git_repo + "." 

725 if model.git_repo 

726 else "" 

727 } 

728 

729## Out-of-Scope Use 

730 

731{out_of_scope_use} 

732 

733 

734{model.config.bioimageio.bias_risks_limitations.format_md()} 

735 

736# How to Get Started with the Model 

737 

738You can use "huggingface/{repo_id}/{ 

739 model.version or "draft" 

740 }" as the resource identifier to load this model directly from the Hugging Face Hub using bioimageio.spec or bioimageio.core. 

741 

742See [bioimageio.core documentation: Get started](https://bioimage-io.github.io/core-bioimage-io-python/latest/get-started) for instructions on how to load and run this model using the `bioimageio.core` Python package or the bioimageio CLI. 

743 

744# Training Details 

745 

746## Training Data 

747 

748{ 

749 "This model was trained on `" + str(model.training_data.id) + "`." 

750 if model.training_data is not None 

751 else "missing" 

752 } 

753 

754## Training Procedure 

755 

756{training_details} 

757 

758{speeds_sizes_times} 

759{evaluation} 

760{environmental_impact} 

761 

762# Technical Specifications 

763 

764{model_arch_and_objective} 

765 

766## Compute Infrastructure 

767 

768{hardware_requirements} 

769 

770### Software 

771 

772- **Framework:** {dl_frameworks} 

773- **Libraries:** {dependencies} 

774- **BioImage.IO partner compatibility:** [Compatibility Reports](https://bioimage-io.github.io/collection/latest/compatibility/#compatibility-by-resource) 

775 

776--- 

777 

778*This model card was created using the template of the bioimageio.spec Python Package, which intern is based on the BioImage Model Zoo template, incorporating best practices from the Hugging Face Model Card Template. For more information on contributing models, visit [bioimage.io](https://bioimage.io).* 

779 

780--- 

781 

782**References:** 

783 

784- [Hugging Face Model Card Template](https://huggingface.co/docs/hub/en/model-card-annotated) 

785- [Hugging Face modelcard_template.md](https://github.com/huggingface/huggingface_hub/blob/b9decfdf9b9a162012bc52f260fd64fc37db660e/src/huggingface_hub/templates/modelcard_template.md) 

786- [BioImage Model Zoo Documentation](https://bioimage.io/docs/) 

787- [Model Cards for Model Reporting](https://arxiv.org/abs/1810.03993) 

788- [bioimageio.spec Python Package](https://bioimage-io.github.io/spec-bioimage-io) 

789""" 

790 

791 return readme, referenced_files