Spaces:
Build error
Build error
File size: 5,796 Bytes
bd09498 e0fd571 bd09498 eec7490 bd09498 021c7a8 bd09498 eec7490 bd09498 f842a06 bd09498 f8ed5d0 9097545 bd09498 4d7e198 e0fd571 4d7e198 bd09498 9097545 f8ed5d0 9097545 bd09498 e0fd571 bd09498 965e8e0 bd09498 965e8e0 d03b796 bd09498 eec7490 bd09498 ed52286 f842a06 eec7490 27355ae eec7490 021c7a8 eec7490 d03b796 9097545 eec7490 | 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 | """
Application FastAPI — point d'entrée de IIIF Studio.
Tous les endpoints sont sous /api/v1/ (R10).
CORS ouvert pour le développement local (origins=["*"]).
La BDD SQLite est créée automatiquement au démarrage (lifespan).
"""
# 1. stdlib
import logging
from contextlib import asynccontextmanager
from pathlib import Path
# 2. third-party
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse, RedirectResponse
# 3. local — on importe les modèles pour que Base.metadata les connaisse
import app.models # noqa: F401 (enregistrement des modèles SQLAlchemy)
from app.api.v1 import corpora, export, ingest, jobs, manuscripts, models_api, pages, profiles, search
from app.models.database import Base, engine
logger = logging.getLogger(__name__)
def _migrate_model_configs(connection) -> None:
"""Ajoute la colonne supports_vision si absente (migration BDD existantes)."""
from sqlalchemy import inspect, text
inspector = inspect(connection)
columns = {c["name"] for c in inspector.get_columns("model_configs")}
if "supports_vision" not in columns:
connection.execute(
text("ALTER TABLE model_configs ADD COLUMN supports_vision BOOLEAN NOT NULL DEFAULT 1")
)
logger.info("Migration : colonne supports_vision ajoutée à model_configs")
def _migrate_page_search(connection) -> None:
"""Ajoute la colonne normalized_text si absente (recherche SQL LIKE)."""
from sqlalchemy import inspect, text
inspector = inspect(connection)
if "page_search" not in inspector.get_table_names():
return
columns = {c["name"] for c in inspector.get_columns("page_search")}
if "normalized_text" not in columns:
connection.execute(
text("ALTER TABLE page_search ADD COLUMN normalized_text TEXT NOT NULL DEFAULT ''")
)
logger.info("Migration : colonne normalized_text ajoutée à page_search")
@asynccontextmanager
async def lifespan(application: FastAPI):
"""Crée les tables SQLite au démarrage, libère l'engine à l'arrêt."""
from app.config import settings
# ── Diagnostic des chemins au démarrage ──────────────────────────────────
# Ces logs apparaissent dans la console HuggingFace et permettent de
# diagnostiquer instantanément tout problème de chemin en production.
logger.info(
"Démarrage IIIF Studio — chemins configurés",
extra={
"profiles_dir": str(settings.profiles_dir),
"profiles_dir_ok": settings.profiles_dir.is_dir(),
"prompts_dir": str(settings.prompts_dir),
"prompts_dir_ok": settings.prompts_dir.is_dir(),
"data_dir": str(settings.data_dir),
"data_dir_ok": settings.data_dir.exists(),
},
)
if not settings.profiles_dir.is_dir():
logger.error(
"ERREUR CRITIQUE : profiles_dir introuvable au démarrage. "
"GET /api/v1/profiles retournera []. "
"Vérifier PROFILES_DIR ou la structure du container.",
extra={"profiles_dir": str(settings.profiles_dir)},
)
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
# Migrations : create_all ne fait pas d'ALTER TABLE sur les tables existantes
await conn.run_sync(_migrate_model_configs)
await conn.run_sync(_migrate_page_search)
logger.info("Tables SQLite initialisées")
yield
await engine.dispose()
logger.info("Engine SQLite fermé")
app = FastAPI(
title="IIIF Studio",
description="Plateforme générique de génération d'éditions savantes augmentées",
version="0.1.0",
lifespan=lifespan,
)
# ── CORS (configurable via CORS_ORIGINS ; défaut : ["*"] pour le dev) ─────────
from app.config import settings as _settings
app.add_middleware(
CORSMiddleware,
allow_origins=_settings.cors_origins,
allow_credentials=False,
allow_methods=["*"],
allow_headers=["*"],
)
# ── Routers (préfixe /api/v1/ — R10) ─────────────────────────────────────────
_V1_PREFIX = "/api/v1"
app.include_router(corpora.router, prefix=_V1_PREFIX)
app.include_router(manuscripts.router, prefix=_V1_PREFIX)
app.include_router(pages.router, prefix=_V1_PREFIX)
app.include_router(export.router, prefix=_V1_PREFIX)
app.include_router(profiles.router, prefix=_V1_PREFIX)
app.include_router(jobs.router, prefix=_V1_PREFIX)
app.include_router(ingest.router, prefix=_V1_PREFIX)
app.include_router(models_api.router, prefix=_V1_PREFIX)
app.include_router(search.router, prefix=_V1_PREFIX)
# ── Serving frontend SPA (production) ou redirect /docs (dev) ────────────────
_STATIC_DIR = Path("/app/static")
@app.get("/{full_path:path}", include_in_schema=False, response_model=None)
async def serve_frontend(full_path: str) -> FileResponse | RedirectResponse:
"""En production sert le frontend React (SPA). En dev redirige vers /docs."""
if full_path.startswith("api/"):
raise HTTPException(status_code=404, detail=f"Endpoint not found: /{full_path}")
if _STATIC_DIR.is_dir():
candidate = (_STATIC_DIR / full_path).resolve()
# Empêcher le path traversal : le fichier résolu doit être sous _STATIC_DIR
if candidate.is_file() and candidate.is_relative_to(_STATIC_DIR.resolve()):
return FileResponse(candidate)
index = _STATIC_DIR / "index.html"
if index.exists():
return FileResponse(index)
return RedirectResponse(url="/docs")
|