cora / etymology_api.py
tokgae's picture
Upload folder using huggingface_hub
38ab39c verified
"""
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)