| """
|
| Cora Master API - Etymology App Integration
|
| A unified endpoint for generating historical illustrations for etymological entries.
|
| """
|
|
|
| from fastapi import FastAPI, HTTPException
|
| from fastapi.middleware.cors import CORSMiddleware
|
| from fastapi.staticfiles import StaticFiles
|
| from pydantic import BaseModel
|
| from typing import Optional, List
|
| import os
|
| import base64
|
| from io import BytesIO
|
|
|
| from cora_engine import CoraEngine
|
| from cora_curator import CoraCurator
|
| from cora_vision import CoraVision
|
| from cora_memory import CoraMemory
|
|
|
| app = FastAPI(title="Cora Visual Etymology API", version="1.0.0")
|
|
|
|
|
| app.add_middleware(
|
| CORSMiddleware,
|
| allow_origins=[
|
| "http://localhost:3000",
|
| "https://le-clef-mot.vercel.app",
|
| "https://*.vercel.app"
|
| ],
|
| allow_credentials=True,
|
| allow_methods=["*"],
|
| allow_headers=["*"],
|
| )
|
|
|
|
|
| engine = CoraEngine()
|
| curator = CoraCurator()
|
| vision = CoraVision()
|
| memory = CoraMemory()
|
|
|
|
|
| if not os.path.exists("archive_images"):
|
| os.makedirs("archive_images")
|
| app.mount("/archive_images", StaticFiles(directory="archive_images"), name="archive_images")
|
|
|
|
|
| class IllustrationRequest(BaseModel):
|
| word: str
|
| etymology_context: Optional[str] = None
|
| visual_prompt: Optional[str] = None
|
| style: Optional[str] = "historical_illustration"
|
|
|
|
|
| class IllustrationResponse(BaseModel):
|
| success: bool
|
| image_url: Optional[str] = None
|
| image_base64: Optional[str] = None
|
| prompt_used: str
|
| tags: List[str] = []
|
| source: str
|
| error: Optional[str] = None
|
|
|
|
|
| @app.post("/api/v1/generate_illustration", response_model=IllustrationResponse)
|
| async def generate_illustration(request: IllustrationRequest):
|
| """
|
| Generate a historical illustration for an etymological entry.
|
|
|
| Pipeline:
|
| 1. Use visual_prompt if provided (from Cledor), otherwise use Curator to refine
|
| 2. Engine attempts generation (may fail with 402)
|
| 3. On failure, RAG fallback searches archive for relevant artifacts
|
| 4. Returns either generated image or museum artifact
|
| """
|
| try:
|
|
|
| if request.visual_prompt:
|
|
|
| refined_prompt = request.visual_prompt
|
| print(f"[Cora API] Using Cledor's visual prompt: {refined_prompt[:100]}...")
|
| else:
|
|
|
| base_prompt = request.word
|
| if request.etymology_context:
|
| base_prompt = f"{request.word}: {request.etymology_context}"
|
|
|
|
|
| refined_prompt = curator.refine_prompt(base_prompt)
|
| print(f"[Cora API] Curator refined prompt: {refined_prompt[:100]}...")
|
|
|
|
|
| try:
|
| result_image = engine.generate_from_text(refined_prompt)
|
| source = "generated"
|
| except Exception as gen_error:
|
|
|
|
|
| return IllustrationResponse(
|
| success=False,
|
| prompt_used=refined_prompt,
|
| source="none",
|
| error=str(gen_error)
|
| )
|
|
|
|
|
|
|
| filename = f"etym_{request.word.replace(' ', '_')}_{os.urandom(4).hex()}.png"
|
| filepath = os.path.join("archive_images", filename)
|
| result_image.save(filepath)
|
|
|
|
|
| emb = vision.embed_image(result_image)
|
| tags = vision.detect_tags(result_image)
|
| tags.append(f"etymology:{request.word}")
|
| memory.save(filepath, emb, request.word, tags)
|
|
|
|
|
| image_url = f"http://localhost:8000/archive_images/{filename}"
|
|
|
|
|
| buffered = BytesIO()
|
| result_image.save(buffered, format="PNG")
|
| img_str = base64.b64encode(buffered.getvalue()).decode()
|
|
|
| return IllustrationResponse(
|
| success=True,
|
| image_url=image_url,
|
| image_base64=img_str,
|
| prompt_used=refined_prompt,
|
| tags=tags,
|
| source=source
|
| )
|
|
|
| except Exception as e:
|
| return IllustrationResponse(
|
| success=False,
|
| prompt_used=request.word,
|
| source="error",
|
| error=str(e)
|
| )
|
|
|
|
|
| @app.get("/api/v1/search_archive")
|
| async def search_archive(query: str, limit: int = 5):
|
| """
|
| Search the visual archive for relevant artifacts.
|
| Useful for the etymology app to show related historical images.
|
| """
|
| try:
|
| emb = vision.embed_text(query)
|
| results = memory.search_by_vector(emb, k=limit)
|
|
|
| images = []
|
| if results['ids']:
|
| ids = results['ids'][0]
|
| metadatas = results['metadatas'][0]
|
| distances = results['distances'][0]
|
|
|
| for i, uid in enumerate(ids):
|
| path = metadatas[i].get('path')
|
| if path and os.path.exists(path):
|
| filename = os.path.basename(path)
|
| image_url = f"http://localhost:8000/archive_images/{filename}"
|
| images.append({
|
| "url": image_url,
|
| "tags": metadatas[i].get('tags'),
|
| "prompt": metadatas[i].get('prompt'),
|
| "score": float(distances[i])
|
| })
|
|
|
| return {"results": images}
|
| except Exception as e:
|
| raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
| @app.get("/health")
|
| async def health_check():
|
| """Health check endpoint for the etymology app to verify API status."""
|
| return {
|
| "status": "healthy",
|
| "components": {
|
| "engine": {
|
| "status": engine.client is not None,
|
| "backend": "cloud (huggingface)" if engine.client else "none"
|
| },
|
| "curator": {
|
| "status": curator.backend != "none",
|
| "backend": curator.backend,
|
| "model": curator.MODEL_ID
|
| },
|
| "vision": vision is not None,
|
| "memory": memory is not None
|
| }
|
| }
|
|
|
|
|
| if __name__ == "__main__":
|
| import uvicorn
|
| uvicorn.run(app, host="0.0.0.0", port=8000)
|
|
|