Spaces:
Build error
Build error
File size: 8,308 Bytes
9bb793c 2d76892 9bb793c 2d76892 9bb793c 9b4e099 9bb793c 193eb98 cd353f9 2d76892 9bb793c 9097545 9bb793c 9b4e099 9bb793c 9b4e099 9bb793c 9b4e099 9bb793c 9b4e099 9bb793c cd353f9 9bb793c cd353f9 9bb793c 9b4e099 9bb793c 2d76892 9bb793c 2d76892 9bb793c 2d76892 9bb793c 2d76892 d82da85 9bb793c cd353f9 9bb793c 9b4e099 9bb793c 9b4e099 9bb793c 9b4e099 9bb793c 193eb98 9bb793c 9b4e099 9bb793c | 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 191 192 193 194 195 196 197 198 199 | """
Analyse primaire IA d'un folio : appel provider IA + รฉcriture master.json (R02, R04, R05).
Point d'entrรฉe : run_primary_analysis().
Chaรฎne : prompt_loader โ model_registry โ provider.generate_content โ master_writer โ response_parser.
"""
# 1. stdlib
import logging
from datetime import datetime, timezone
from pathlib import Path
# 3. local
from app.schemas.corpus_profile import CorpusProfile
from app.schemas.image import ImageDerivativeInfo, ImageSourceInfo
from app.schemas.model_config import ModelConfig
from app.schemas.page_master import EditorialInfo, EditorialStatus, ImageInfo, PageMaster, ProcessingInfo
from app.services.ai.master_writer import write_ai_raw, write_master_json
from app.services.ai.model_registry import get_provider
from app.services.ai.prompt_loader import load_and_render_prompt
from app.services.ai.response_parser import parse_ai_response
logger = logging.getLogger(__name__)
def _scale_bbox_coordinates(layout: dict, scale_x: float, scale_y: float) -> dict:
"""Met ร l'รฉchelle les bbox de l'espace dรฉrivรฉ vers l'espace canvas original.
L'IA analyse un dรฉrivรฉ 1500px mais les coordonnรฉes dans master.json
doivent รชtre en pixels absolus du canvas original (convention IIIF).
"""
if abs(scale_x - 1.0) < 0.01 and abs(scale_y - 1.0) < 0.01:
return layout # pas de scaling nรฉcessaire
regions = layout.get("regions", [])
for region in regions:
bbox = region.get("bbox")
if bbox and len(bbox) == 4:
region["bbox"] = [
round(bbox[0] * scale_x),
round(bbox[1] * scale_y),
round(bbox[2] * scale_x),
round(bbox[3] * scale_y),
]
return layout
def run_primary_analysis(
*,
derivative_image_bytes: bytes | None = None,
derivative_image_path: Path | None = None,
corpus_profile: CorpusProfile,
model_config: ModelConfig,
page_id: str,
manuscript_id: str,
corpus_slug: str,
folio_label: str,
sequence: int,
image_info: ImageDerivativeInfo | ImageSourceInfo,
derivative_width: int | None = None,
derivative_height: int | None = None,
base_data_dir: Path = Path("data"),
project_root: Path = Path("."),
) -> PageMaster:
"""Analyse primaire d'un folio : charge le prompt, appelle l'IA, รฉcrit les fichiers.
Supporte deux modes :
- IIIF natif : derivative_image_bytes fourni (bytes en RAM, jamais sur disque)
- Legacy : derivative_image_path fourni (chemin fichier sur disque)
Respecte R05 : ai_raw.json toujours รฉcrit en premier.
Si les dimensions originales (canvas) diffรจrent du dรฉrivรฉ, les bbox sont
mises ร l'รฉchelle de l'espace dรฉrivรฉ vers l'espace canvas original.
"""
# โโ Chemins de sortie โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
page_dir = base_data_dir / "corpora" / corpus_slug / "pages" / folio_label
raw_path = page_dir / "ai_raw.json"
master_path = page_dir / "master.json"
# โโ 1. Chargement et rendu du prompt (R04) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
prompt_rel_path: str = corpus_profile.prompt_templates["primary"]
prompt_abs_path = project_root / prompt_rel_path
context = {
"profile_label": corpus_profile.label,
"language_hints": ", ".join(corpus_profile.language_hints),
"primary_language": corpus_profile.language_hints[0] if corpus_profile.language_hints else "la",
"script_type": corpus_profile.script_type.value,
}
prompt_text = load_and_render_prompt(prompt_abs_path, context)
logger.info(
"Prompt rendu",
extra={"template": prompt_rel_path, "corpus": corpus_slug, "folio": folio_label},
)
# โโ 2. Obtention des bytes image โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
if derivative_image_bytes is not None:
jpeg_bytes = derivative_image_bytes
elif derivative_image_path is not None:
if not derivative_image_path.exists():
raise FileNotFoundError(f"Image dรฉrivรฉe introuvable : {derivative_image_path}")
try:
jpeg_bytes = derivative_image_path.read_bytes()
except OSError as exc:
raise RuntimeError(f"Erreur lecture image {derivative_image_path} : {exc}") from exc
else:
raise ValueError("Il faut fournir derivative_image_bytes ou derivative_image_path")
# โโ 3. Appel IA via le provider sรฉlectionnรฉ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
provider = get_provider(model_config.provider)
logger.info(
"Appel IA",
extra={
"provider": model_config.provider.value,
"model": model_config.selected_model_id,
"corpus": corpus_slug,
"folio": folio_label,
},
)
raw_text = provider.generate_content(
image_bytes=jpeg_bytes,
prompt=prompt_text,
model_id=model_config.selected_model_id,
supports_vision=model_config.supports_vision,
)
# โโ 4. รcriture ai_raw.json TOUJOURS EN PREMIER (R05) โโโโโโโโโโโโโโโโโ
write_ai_raw(raw_text, raw_path)
# โโ 5. Parsing + validation (ParseError si JSON invalide) โโโโโโโโโโโโโโโ
layout, ocr = parse_ai_response(raw_text)
# โโ 5b. Scaling bbox si les dimensions originales diffรจrent du dรฉrivรฉ โโ
is_iiif_source = isinstance(image_info, ImageSourceInfo)
original_w = image_info.original_width
original_h = image_info.original_height
deriv_w = derivative_width or (getattr(image_info, "derivative_width", None)) or original_w
deriv_h = derivative_height or (getattr(image_info, "derivative_height", None)) or original_h
if original_w > 0 and deriv_w > 0 and (original_w != deriv_w or original_h != deriv_h):
scale_x = original_w / deriv_w
scale_y = original_h / deriv_h
layout = _scale_bbox_coordinates(layout, scale_x, scale_y)
# โโ 6. Construction du PageMaster โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
processed_at = datetime.now(tz=timezone.utc)
if is_iiif_source:
image_block = ImageInfo(
master=image_info.original_url,
iiif_service_url=image_info.iiif_service_url,
manifest_url=image_info.manifest_url,
width=original_w,
height=original_h,
)
else:
image_block = ImageInfo(
master=image_info.original_url,
derivative_web=getattr(image_info, "derivative_path", None),
thumbnail=getattr(image_info, "thumbnail_path", None),
width=original_w,
height=original_h,
)
page_master = PageMaster(
page_id=page_id,
corpus_profile=corpus_profile.profile_id,
manuscript_id=manuscript_id,
folio_label=folio_label,
sequence=sequence,
image=image_block,
layout=layout,
ocr=ocr,
processing=ProcessingInfo(
provider=model_config.provider.value if hasattr(model_config.provider, "value") else str(model_config.provider),
model_id=model_config.selected_model_id,
model_display_name=model_config.selected_model_display_name,
prompt_version=prompt_rel_path,
raw_response_path=str(raw_path),
processed_at=processed_at,
),
editorial=EditorialInfo(status=EditorialStatus.MACHINE_DRAFT),
)
# โโ 7. รcriture master.json (seulement si parsing OK) โโโโโโโโโโโโโโโโโโโ
write_master_json(page_master, master_path)
logger.info(
"Analyse primaire terminรฉe",
extra={
"page_id": page_id,
"corpus": corpus_slug,
"folio": folio_label,
"regions": len(layout.get("regions", [])),
"iiif_native": is_iiif_source,
},
)
return page_master
|