""" 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") # Enable CORS for etymology app integration app.add_middleware( CORSMiddleware, allow_origins=[ "http://localhost:3000", # Next.js development "https://le-clef-mot.vercel.app", # Production (update with your domain) "https://*.vercel.app" # Vercel preview deployments ], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # Initialize components engine = CoraEngine() curator = CoraCurator() vision = CoraVision() memory = CoraMemory() # Mount static files 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 # NEW: Accept visual prompt from Cledor style: Optional[str] = "historical_illustration" # or "daily_life", "epic_dimension" class IllustrationResponse(BaseModel): success: bool image_url: Optional[str] = None image_base64: Optional[str] = None prompt_used: str tags: List[str] = [] source: str # "generated" or "archive" 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: # Step 1: Determine prompt to use if request.visual_prompt: # Cledor already generated the prompt, use it directly refined_prompt = request.visual_prompt print(f"[Cora API] Using Cledor's visual prompt: {refined_prompt[:100]}...") else: # Fallback: Build prompt from etymology data base_prompt = request.word if request.etymology_context: base_prompt = f"{request.word}: {request.etymology_context}" # Curator refinement refined_prompt = curator.refine_prompt(base_prompt) print(f"[Cora API] Curator refined prompt: {refined_prompt[:100]}...") # Step 2: Attempt generation (with built-in RAG fallback) try: result_image = engine.generate_from_text(refined_prompt) source = "generated" except Exception as gen_error: # RAG fallback is already built into engine.generate_from_text # If we reach here, both generation and fallback failed return IllustrationResponse( success=False, prompt_used=refined_prompt, source="none", error=str(gen_error) ) # Step 4: Save to archive and generate response # Save image filename = f"etym_{request.word.replace(' ', '_')}_{os.urandom(4).hex()}.png" filepath = os.path.join("archive_images", filename) result_image.save(filepath) # Archive in memory 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) # Generate URL image_url = f"http://localhost:8000/archive_images/{filename}" # Optionally encode as base64 for direct embedding 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)