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

304 statements  

« prev     ^ index     » next       coverage.py v7.15.4, created at 2026-08-18 09:17 +0000

1from __future__ import annotations 

2 

3import collections.abc 

4import warnings 

5from functools import partial 

6from pathlib import PurePosixPath 

7from typing import Any, Sequence 

8 

9import numpy as np 

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

11from loguru import logger 

12from numpy.typing import NDArray 

13from typing_extensions import assert_never 

14 

15from bioimageio.spec._internal.validation_context import get_validation_context 

16from bioimageio.spec.model.v0_5 import ( 

17 FileDescr, 

18 IntervalOrRatioDataDescr, 

19 KerasHdf5WeightsDescr, 

20 KerasV3WeightsDescr, 

21 NominalOrOrdinalDataDescr, 

22 OnnxWeightsDescr, 

23 PytorchStateDictWeightsDescr, 

24 TensorflowJsWeightsDescr, 

25 TensorflowSavedModelBundleWeightsDescr, 

26 TensorId, 

27 TorchscriptWeightsDescr, 

28) 

29 

30from ._internal.io import RelativeFilePath, get_reader 

31from ._internal.io_utils import load_array 

32from ._version import VERSION 

33from .model import ModelDescr 

34from .utils import get_spdx_licenses, load_image 

35 

36HF_KNOWN_LICENSES = ( 

37 "apache-2.0", 

38 "mit", 

39 "openrail", 

40 "bigscience-openrail-m", 

41 "creativeml-openrail-m", 

42 "bigscience-bloom-rail-1.0", 

43 "bigcode-openrail-m", 

44 "afl-3.0", 

45 "artistic-2.0", 

46 "bsl-1.0", 

47 "bsd", 

48 "bsd-2-clause", 

49 "bsd-3-clause", 

50 "bsd-3-clause-clear", 

51 "c-uda", 

52 "cc", 

53 "cc0-1.0", 

54 "cc-by-2.0", 

55 "cc-by-2.5", 

56 "cc-by-3.0", 

57 "cc-by-4.0", 

58 "cc-by-sa-3.0", 

59 "cc-by-sa-4.0", 

60 "cc-by-nc-2.0", 

61 "cc-by-nc-3.0", 

62 "cc-by-nc-4.0", 

63 "cc-by-nd-4.0", 

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

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

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

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

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

69 "cdla-sharing-1.0", 

70 "cdla-permissive-1.0", 

71 "cdla-permissive-2.0", 

72 "wtfpl", 

73 "ecl-2.0", 

74 "epl-1.0", 

75 "epl-2.0", 

76 "etalab-2.0", 

77 "eupl-1.1", 

78 "eupl-1.2", 

79 "agpl-3.0", 

80 "gfdl", 

81 "gpl", 

82 "gpl-2.0", 

83 "gpl-3.0", 

84 "lgpl", 

85 "lgpl-2.1", 

86 "lgpl-3.0", 

87 "isc", 

88 "h-research", 

89 "intel-research", 

90 "lppl-1.3c", 

91 "ms-pl", 

92 "apple-ascl", 

93 "apple-amlr", 

94 "mpl-2.0", 

95 "odc-by", 

96 "odbl", 

97 "openmdw-1.0", 

98 "openrail++", 

99 "osl-3.0", 

100 "postgresql", 

101 "ofl-1.1", 

102 "ncsa", 

103 "unlicense", 

104 "zlib", 

105 "pddl", 

106 "lgpl-lr", 

107 "deepfloyd-if-license", 

108 "fair-noncommercial-research-license", 

109 "llama2", 

110 "llama3", 

111 "llama3.1", 

112 "llama3.2", 

113 "llama3.3", 

114 "llama4", 

115 "grok2-community", 

116 "gemma", 

117) 

118 

119 

120def _generate_png_from_tensor(tensor: NDArray[np.generic]) -> bytes | None: 

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

122 

123 Prefers 2D slices from multi-dimensional arrays. 

124 Returns PNG bytes or None if generation fails. 

125 """ 

126 try: 

127 # Squeeze out singleton dimensions 

128 arr = np.squeeze(tensor) 

129 

130 # Handle different dimensionalities 

131 if arr.ndim == 2: 

132 img_data = arr 

133 elif arr.ndim == 3: 

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

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

136 img_data = arr 

137 else: # Take middle slice 

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

139 elif arr.ndim == 4: 

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

141 img_data = ( 

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

143 if arr.shape[0] == 1 

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

145 ) 

146 elif arr.ndim > 4: 

147 # Take middle slices of all extra dimensions 

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

149 img_data = arr[slices] 

150 else: 

151 return None 

152 

153 # Normalize to 0-255 uint8 

154 img_data = np.squeeze(img_data) 

155 if img_data.dtype != np.uint8: 

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

157 if img_max > img_min: 

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

159 else: 

160 img_data = np.zeros_like(img_data) 

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

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

163 except Exception: 

164 return None 

165 

166 

167def _get_io_description( 

168 model: ModelDescr, 

169) -> tuple[str, dict[str, bytes], list[TensorId], list[TensorId]]: 

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

171 

172 Returns: 

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

174 filenames to file bytes. 

175 """ 

176 markdown_string = "" 

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

178 input_ids: list[TensorId] = [] 

179 output_ids: list[TensorId] = [] 

180 

181 def format_data_descr( 

182 d: NominalOrOrdinalDataDescr 

183 | IntervalOrRatioDataDescr 

184 | Sequence[NominalOrOrdinalDataDescr | IntervalOrRatioDataDescr], 

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(str(a.size) for a in inp.axes) 

214 

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

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

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

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

219 markdown_string += format_data_descr(inp.data) 

220 

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

222 img_bytes = None 

223 if inp.sample_tensor is not None: 

224 try: 

225 arr = np.asarray(load_image(inp.sample_tensor)) 

226 img_bytes = _generate_png_from_tensor(arr) 

227 except Exception as e: 

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

229 

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

231 try: 

232 arr = load_array(inp.test_tensor) 

233 img_bytes = _generate_png_from_tensor(arr) 

234 except Exception as e: 

235 logger.error( 

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

237 ) 

238 

239 if img_bytes: 

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

241 referenced_files[filename] = img_bytes 

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

243 

244 # Output descriptions 

245 if model.outputs: 

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

247 for out in model.outputs: 

248 output_ids.append(out.id) 

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

250 shape_str = " × ".join(str(a.size) for a in out.axes) 

251 

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

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

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

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

256 markdown_string += format_data_descr(out.data) 

257 

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

259 img_bytes = None 

260 if out.sample_tensor is not None: 

261 try: 

262 arr = np.asarray(load_image(out.sample_tensor)) 

263 img_bytes = _generate_png_from_tensor(arr) 

264 except Exception as e: 

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

266 

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

268 try: 

269 arr = load_array(out.test_tensor) 

270 img_bytes = _generate_png_from_tensor(arr) 

271 except Exception as e: 

272 logger.error( 

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

274 ) 

275 

276 if img_bytes: 

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

278 referenced_files[filename] = img_bytes 

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

280 

281 return markdown_string, referenced_files, input_ids, output_ids 

282 

283 

284def create_huggingface_model_card( 

285 model: ModelDescr, *, repo_id: str 

286) -> tuple[str, dict[str, bytes]]: 

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

288 

289 Returns: 

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

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

292 """ 

293 model = model.model_copy() 

294 

295 if model.version is None: 

296 model_version = "" 

297 else: 

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

299 

300 if model.documentation is None: 

301 additional_model_doc = "" 

302 else: 

303 doc_reader = get_reader(model.documentation) 

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

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

306 model.documentation = FileDescr( 

307 source=RelativeFilePath(PurePosixPath(local_doc_path)) 

308 ) 

309 

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

311 

312 if model.cite: 

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

314 "".join( 

315 ( 

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

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

318 ) 

319 for c in model.cite 

320 ) 

321 ) 

322 else: 

323 developed_by = "" 

324 

325 if model.config.bioimageio.funded_by: 

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

327 else: 

328 funded_by = "" 

329 

330 if model.authors: 

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

332 "".join( 

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

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

335 + ( 

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

337 if a.orcid 

338 else "" 

339 ) 

340 + ( 

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

342 if a.github_user 

343 else "" 

344 ) 

345 for a in model.authors 

346 ) 

347 ) 

348 else: 

349 shared_by = "" 

350 

351 if model.config.bioimageio.architecture_type: 

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

353 else: 

354 model_type = "" 

355 

356 if model.config.bioimageio.modality: 

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

358 else: 

359 model_modality = "" 

360 

361 if model.config.bioimageio.target_structure: 

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

363 model.config.bioimageio.target_structure 

364 ) 

365 else: 

366 target_structures = "" 

367 

368 if model.config.bioimageio.task: 

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

370 else: 

371 task_type = "" 

372 

373 if model.parent: 

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

375 else: 

376 finetuned_from = "" 

377 

378 repository = ( 

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

380 ) 

381 

382 dl_framework_parts: list[str] = [] 

383 training_frameworks: list[str] = [] 

384 model_size: str | None = None 

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

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

387 dl_framework_version = weights.pytorch_version 

388 elif isinstance( 

389 weights, 

390 ( 

391 TensorflowSavedModelBundleWeightsDescr, 

392 TensorflowJsWeightsDescr, 

393 KerasHdf5WeightsDescr, 

394 ), 

395 ): 

396 dl_framework_version = weights.tensorflow_version 

397 elif isinstance(weights, KerasV3WeightsDescr): 

398 dl_framework_version = weights.keras_version 

399 elif isinstance(weights, OnnxWeightsDescr): 

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

401 else: 

402 assert_never(weights) 

403 

404 if weights.parent is None: 

405 training_frameworks.append(weights.weights_format_name) 

406 

407 dl_framework_parts.append( 

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

409 ) 

410 

411 if model_size is None: 

412 s = 0 

413 r = weights.get_reader() 

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

415 s += len(chunk) 

416 

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

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

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

420 else: 

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

422 else: 

423 model_size = "" 

424 

425 if s < 1e9: 

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

427 else: 

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

429 

430 dl_frameworks = "".join(dl_framework_parts) 

431 if len(training_frameworks) > 1: 

432 warnings.warn( 

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

434 ) 

435 

436 if ( 

437 model.weights.pytorch_state_dict is not None 

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

439 ): 

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

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

442 else: 

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

444 

445 out_of_scope_use = ( 

446 model.config.bioimageio.out_of_scope_use 

447 if model.config.bioimageio.out_of_scope_use 

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

449 

450- *Likely not suitable for diagnostic purposes.* 

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

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

453 

454""" 

455 ) 

456 

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

458 if environmental_impact: 

459 environmental_impact_toc_entry = ( 

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

461 ) 

462 else: 

463 environmental_impact_toc_entry = "" 

464 

465 evaluation_parts: list[str] = [] 

466 n_evals = 0 

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

468 if e.dataset_role == "independent": 

469 continue # treated separately below 

470 

471 n_evals += 1 

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

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

474 evaluation_parts.append(e.format_md()) 

475 

476 n_evals = 0 

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

478 if e.dataset_role != "independent": 

479 continue # treated separately above 

480 

481 n_evals += 1 

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

483 

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

485 evaluation_parts.append(e.format_md()) 

486 

487 if evaluation_parts: 

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

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

490 else: 

491 evaluation = "" 

492 evaluation_toc_entry = "" 

493 

494 training_details = "" 

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

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

497 

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

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

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

501 training_details += ( 

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

503 ) 

504 

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

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

507 

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

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

510 

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

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

513 

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

515 training_details += ( 

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

517 ) 

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

519 training_details += ( 

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

521 ) 

522 training_details += "\n" 

523 

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

525 training_details += ( 

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

527 ) 

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

529 training_details += ( 

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

531 ) 

532 training_details += "\n" 

533 

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

535 training_details += ( 

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

537 ) 

538 

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

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

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

542 

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

544 if model.config.bioimageio.inference_time: 

545 speeds_sizes_times += ( 

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

547 ) 

548 

549 if model.config.bioimageio.memory_requirements_inference: 

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

551 

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

553 if ( 

554 model.config.bioimageio.architecture_type 

555 or model.config.bioimageio.architecture_description 

556 ): 

557 model_arch_and_objective += ( 

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

559 + ( 

560 " --- " 

561 if model.config.bioimageio.architecture_type 

562 and model.config.bioimageio.architecture_description 

563 else "" 

564 ) 

565 + ( 

566 model.config.bioimageio.architecture_description 

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

568 else "" 

569 ) 

570 + "\n" 

571 ) 

572 

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

574 predict_snippet_inputs = str( 

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

576 ) 

577 model_arch_and_objective += io_desc 

578 

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

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

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

582 

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

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

585 

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

587 

588 if model.license is None: 

589 license = "unknown" 

590 license_meta = "unknown" 

591 elif isinstance(model.license, FileDescr): 

592 license_reader = get_reader(model.license) 

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

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

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

596 

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

598 license_meta = "unknown" 

599 else: 

600 spdx_licenses = get_spdx_licenses() 

601 matches = [ 

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

603 for entry in spdx_licenses["licenses"] 

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

605 ] 

606 if matches: 

607 if len(matches) > 1: 

608 logger.warning( 

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

610 model.license, 

611 ) 

612 name, reference = matches[0] 

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

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

615 license_meta = model.license.lower() 

616 else: 

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

618 else: 

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

620 license_meta = model.license.lower() 

621 else: 

622 license_meta = "unknown" 

623 

624 license = model.license.lower() 

625 

626 base_model = ( 

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

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

629 else "" 

630 ) 

631 dataset_meta = ( 

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

633 if model.training_data is not None 

634 and model.training_data.id is not None 

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

636 else "" 

637 ) 

638 if model.covers: 

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

640 cover_image_bytes = cover_image_reader.read() 

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

642 referenced_files[cover_image_filename] = cover_image_bytes 

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

644 thumbnail_meta = ( 

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

646 ) 

647 

648 else: 

649 cover_image_md = "" 

650 thumbnail_meta = "" 

651 

652 # TODO: add pipeline_tag to metadata 

653 readme = f"""--- 

654license: {license_meta}{thumbnail_meta} 

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

656language: [en] 

657library_name: bioimageio{base_model}{dataset_meta} 

658--- 

659# {model.name}{cover_image_md} 

660 

661{model.description or ""} 

662 

663 

664# Table of Contents 

665 

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

667- [Uses](#uses) 

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

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

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

671 environmental_impact_toc_entry 

672 } 

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

674 

675 

676# Model Details 

677 

678## Model Description 

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

680 model_modality 

681 }{target_structures}{task_type} 

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

683 

684## Model Sources 

685 

686- **Repository:** {repository} 

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

688 

689# Uses 

690 

691## Direct Use 

692 

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

694 VERSION 

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

696 

697```python 

698from bioimageio.core import predict 

699 

700output_sample = predict( 

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

702 inputs={predict_snippet_inputs}, 

703) 

704 

705output_tensor = output_sample.members["{ 

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

707 }"] 

708xarray_dataarray = output_tensor.data 

709numpy_ndarray = output_tensor.data.to_numpy() 

710``` 

711 

712## Downstream Use 

713 

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

715{ 

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

717 if model.git_repo 

718 else "" 

719 } 

720 

721## Out-of-Scope Use 

722 

723{out_of_scope_use} 

724 

725 

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

727 

728# How to Get Started with the Model 

729 

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

731 model.version or "draft" 

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

733 

734See [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. 

735 

736# Training Details 

737 

738## Training Data 

739 

740{ 

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

742 if model.training_data is not None 

743 else "missing" 

744 } 

745 

746## Training Procedure 

747 

748{training_details} 

749 

750{speeds_sizes_times} 

751{evaluation} 

752{environmental_impact} 

753 

754# Technical Specifications 

755 

756{model_arch_and_objective} 

757 

758## Compute Infrastructure 

759 

760{hardware_requirements} 

761 

762### Software 

763 

764- **Framework:** {dl_frameworks} 

765- **Libraries:** {dependencies} 

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

767 

768--- 

769 

770*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).* 

771 

772--- 

773 

774**References:** 

775 

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

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

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

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

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

781""" 

782 

783 return readme, referenced_files