Spaces:
Build error
Build error
File size: 7,033 Bytes
c48eec0 e0fd571 c48eec0 cd353f9 c48eec0 193eb98 c48eec0 193eb98 6736e17 c48eec0 cd353f9 c48eec0 6736e17 c48eec0 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 | """
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)})
|