Spaces:
Build error
Build error
| """ | |
| GΓ©nΓ©rateur de Manifest IIIF Presentation API 3.0 par manuscrit (R02). | |
| Source canonique : list[PageMaster] uniquement. | |
| 1 Canvas par page β rΓ¨gle absolue. | |
| Les couches Γ©ditoriales (transcription, commentaire, iconographie) sont | |
| hors périmètre MVP : elles seront ajoutées en Sprint 4 via des annotations. | |
| Structure du manifest : | |
| @context β http://iiif.io/api/presentation/3/context.json | |
| id β {base_url}/api/v1/manuscripts/{manuscript_id}/iiif-manifest | |
| items β Canvas par page (triΓ©s par sequence) | |
| ββ AnnotationPage | |
| ββ Annotation painting (image originale) | |
| """ | |
| # 1. stdlib | |
| import json | |
| import logging | |
| from pathlib import Path | |
| # 3. local | |
| from app.schemas.page_master import PageMaster | |
| logger = logging.getLogger(__name__) | |
| _IIIF_CONTEXT = "http://iiif.io/api/presentation/3/context.json" | |
| def _meta_entry(label_en: str, value: str) -> dict: | |
| """Formate une entrΓ©e de mΓ©tadonnΓ©e IIIF (label/value pair).""" | |
| return { | |
| "label": {"en": [label_en]}, | |
| "value": {"none": [value]}, | |
| } | |
| def generate_manifest( | |
| masters: list[PageMaster], | |
| manuscript_meta: dict, | |
| base_url: str, | |
| ) -> dict: | |
| """Génère un Manifest IIIF Presentation API 3.0 pour un manuscrit. | |
| Le manifest est sΓ©rialisable en JSON sans perte (pas d'objets non-sΓ©rialisables). | |
| Le base_url est paramΓ©trable pour permettre le dΓ©ploiement sur diffΓ©rents domaines. | |
| Args: | |
| masters: liste des PageMaster du manuscrit (au moins 1, triΓ©s par sequence). | |
| manuscript_meta: dict avec les clΓ©s : | |
| Obligatoires : manuscript_id (str), label (str), corpus_slug (str) | |
| Optionnelles : language (str), repository (str), shelfmark (str), | |
| date_label (str), institution (str) | |
| base_url: URL de base de la plateforme, sans slash final | |
| (ex. "https://iiif-studio.hf.space"). | |
| Returns: | |
| dict sΓ©rialisable en JSON contenant le Manifest IIIF 3.0. | |
| Raises: | |
| ValueError: si masters est vide ou si un champ obligatoire est absent. | |
| """ | |
| # ββ Validation βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| if not masters: | |
| raise ValueError( | |
| "generate_manifest : la liste de PageMaster est vide β " | |
| "un manuscrit doit avoir au moins une page." | |
| ) | |
| for key in ("manuscript_id", "label", "corpus_slug"): | |
| if not manuscript_meta.get(key): | |
| raise ValueError( | |
| f"generate_manifest : champ obligatoire manquant dans " | |
| f"manuscript_meta : Β«{key}Β»" | |
| ) | |
| manuscript_id = manuscript_meta["manuscript_id"] | |
| label = manuscript_meta["label"] | |
| language = manuscript_meta.get("language") or "en" | |
| # Pages dans l'ordre de sΓ©quence (rΓ¨gle absolue β structMap PHYSICAL) | |
| pages = sorted(masters, key=lambda m: m.sequence) | |
| # ββ IDs de base βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| base_url = base_url.rstrip("/") | |
| manifest_id = f"{base_url}/api/v1/manuscripts/{manuscript_id}/iiif-manifest" | |
| # ββ MΓ©tadonnΓ©es descriptives βββββββββββββββββββββββββββββββββββββββββββββ | |
| metadata: list[dict] = [] | |
| for field, meta_label in ( | |
| ("repository", "Repository"), | |
| ("shelfmark", "Shelfmark"), | |
| ("date_label", "Date"), | |
| ("institution","Institution"), | |
| ("language", "Language"), | |
| ): | |
| value = manuscript_meta.get(field) | |
| if value: | |
| metadata.append(_meta_entry(meta_label, value)) | |
| # ββ Canvases (1 par page) ββββββββββββββββββββββββββββββββββββββββββββββββ | |
| items: list[dict] = [] | |
| for page in pages: | |
| canvas_id = ( | |
| f"{base_url}/api/v1/manuscripts/{manuscript_id}/canvas/{page.page_id}" | |
| ) | |
| width = page.image.width | |
| height = page.image.height | |
| annotation_page_id = f"{canvas_id}/annotation-page/1" | |
| annotation_id = f"{canvas_id}/annotation/painting" | |
| image_url = page.image.master or "" | |
| iiif_svc = page.image.iiif_service_url | |
| # Corps de l'annotation painting | |
| body: dict = { | |
| "id": image_url, | |
| "type": "Image", | |
| "format": "image/jpeg", | |
| "width": width, | |
| "height": height, | |
| } | |
| # Si un IIIF Image Service est connu, le dΓ©clarer (zoom tuilΓ© natif) | |
| if iiif_svc: | |
| body["service"] = [{ | |
| "id": iiif_svc, | |
| "type": "ImageService3", | |
| "profile": "level2", | |
| }] | |
| canvas: dict = { | |
| "id": canvas_id, | |
| "type": "Canvas", | |
| "label": {language: [f"Folio {page.folio_label}"]}, | |
| "width": width, | |
| "height": height, | |
| "items": [ | |
| { | |
| "id": annotation_page_id, | |
| "type": "AnnotationPage", | |
| "items": [ | |
| { | |
| "id": annotation_id, | |
| "type": "Annotation", | |
| "motivation": "painting", | |
| "body": body, | |
| "target": canvas_id, | |
| } | |
| ], | |
| } | |
| ], | |
| } | |
| items.append(canvas) | |
| manifest: dict = { | |
| "@context": _IIIF_CONTEXT, | |
| "id": manifest_id, | |
| "type": "Manifest", | |
| "label": {language: [label]}, | |
| "metadata": metadata, | |
| "items": items, | |
| } | |
| logger.info( | |
| "Manifest IIIF gΓ©nΓ©rΓ©", | |
| extra={"manuscript_id": manuscript_id, "canvases": len(items)}, | |
| ) | |
| return manifest | |
| def write_manifest( | |
| manifest: dict, | |
| corpus_slug: str, | |
| base_data_dir: Path = Path("data"), | |
| ) -> None: | |
| """Γcrit le Manifest IIIF dans data/corpora/{corpus_slug}/iiif/manifest.json. | |
| CrΓ©e les dossiers parents si nΓ©cessaire. | |
| Args: | |
| manifest: dict retournΓ© par generate_manifest(). | |
| corpus_slug: identifiant du corpus (dΓ©termine le rΓ©pertoire de sortie). | |
| base_data_dir: racine du dossier data. | |
| """ | |
| output_path = base_data_dir / "corpora" / corpus_slug / "iiif" / "manifest.json" | |
| output_path.parent.mkdir(parents=True, exist_ok=True) | |
| output_path.write_text( | |
| json.dumps(manifest, ensure_ascii=False, indent=2), | |
| encoding="utf-8", | |
| ) | |
| logger.info("manifest.json Γ©crit", extra={"path": str(output_path)}) | |