cora / app.py
tokgae's picture
Upload folder using huggingface_hub
273dab5 verified
"""
═══════════════════════════════════════════════════════════
CORA — Unified Entry Point for Hugging Face Spaces
Merges the FastAPI backend + Gradio UI into one process.
═══════════════════════════════════════════════════════════
"""
import os
import io
import uuid
import threading
from dotenv import load_dotenv
load_dotenv()
import gradio as gr
from fastapi import FastAPI, HTTPException, BackgroundTasks
from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel
from PIL import Image
from cora_engine import CoraEngine
from cora_curator import CoraCurator
from cora_vision import CoraVision
from cora_memory import CoraMemory
# ━━━ Initialize Core (Lazy) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━
engine = None
curator = None
vision = None
memory = None
from contextlib import asynccontextmanager
@asynccontextmanager
async def lifespan(app: FastAPI):
global engine, curator, vision, memory
print("🚀 Initializing Cora Engines...")
engine = CoraEngine()
curator = CoraCurator()
vision = CoraVision()
memory = CoraMemory()
yield
print("💤 Shutting down...")
# ━━━ FastAPI Backend ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
api = FastAPI(
title="Cora API",
description="Historical Archive Generator",
lifespan=lifespan
)
ARCHIVE_DIR = os.environ.get("ARCHIVE_DIR", "archive_images")
os.makedirs(ARCHIVE_DIR, exist_ok=True)
class AgentPrompt(BaseModel):
prompt: str
use_curator: bool = True
class SearchQuery(BaseModel):
query: str
limit: int = 10
@api.get("/health")
def health_check():
"""Service status with dynamic backend reporting."""
return {
"status": "online",
"engine": {
"status": engine.client is not None,
"backend": "cloud (huggingface)" if engine.client else "none",
"model": engine.MODEL_ID
},
"curator": {
"status": curator.backend != "none",
"backend": curator.backend,
"model": curator.MODEL_ID
},
"vision": "online" if vision.clip_model else "offline",
"memory": "online" if memory.client else "offline"
}
def archive_generation(image, prompt):
"""Background: save + embed + tag."""
try:
filename = f"{uuid.uuid4()}.png"
filepath = os.path.join(ARCHIVE_DIR, filename)
image.save(filepath)
embedding = vision.embed_image(image)
tags = vision.detect_tags(image)
memory.save(filepath, embedding, prompt, tags)
print(f"✅ Archived: {filepath} | tags: {tags}")
except Exception as e:
print(f"❌ Archive failed: {e}")
@api.post("/agent/generate")
async def agent_generate(request: AgentPrompt, background_tasks: BackgroundTasks):
try:
if not request.prompt or not request.prompt.strip():
raise HTTPException(status_code=400, detail="Prompt cannot be empty.")
final_prompt = request.prompt
if request.use_curator:
try:
final_prompt = curator.refine_prompt(request.prompt)
except Exception:
final_prompt = request.prompt
result = engine.generate_from_text(final_prompt)
if "transparent" in request.prompt.lower():
try:
from rembg import remove
result = remove(result)
except Exception as e:
print(f"Failed to remove background: {e}")
background_tasks.add_task(archive_generation, result, final_prompt)
img_bytes = io.BytesIO()
result.save(img_bytes, format="PNG")
from fastapi.responses import Response
return Response(content=img_bytes.getvalue(), media_type="image/png")
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@api.post("/curator/search")
async def curator_search(request: SearchQuery):
try:
emb = vision.embed_text(request.query)
if not emb:
return {"results": []}
query_lower = request.query.lower()
tag_hints = []
cultural_markers = {
"roman": ["roman", "rome"],
"greek": ["greek", "greece", "hellenic"],
"egyptian": ["egypt", "egyptian"],
"medieval": ["medieval", "middle ages"],
"renaissance": ["renaissance"],
}
for culture, keywords in cultural_markers.items():
if any(kw in query_lower for kw in keywords):
tag_hints.extend(keywords)
if tag_hints:
results = memory.search_hybrid(emb, k=request.limit, tag_filter=tag_hints)
else:
results = memory.search_by_vector(emb, k=request.limit)
images = []
if results["ids"]:
for i, uid in enumerate(results["ids"][0]):
meta = results["metadatas"][0][i]
path = meta.get("path")
if path and os.path.exists(path):
filename = os.path.basename(path)
images.append({
"path": f"/archive_images/{filename}",
"tags": meta.get("tags"),
"prompt": meta.get("prompt"),
"score": float(results["distances"][0][i]),
})
return {"results": images}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
api.mount("/archive_images", StaticFiles(directory=ARCHIVE_DIR), name="archive_images")
# ━━━ Gradio UI ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# Use the in-process API directly instead of HTTP calls
API_URL = None # We call functions directly now
def ui_generate(user_prompt, is_monochromy, monochromy_color, is_transparent, progress=gr.Progress()):
"""Generate an archive illustration — calls engine directly."""
try:
if not user_prompt or not user_prompt.strip():
raise gr.Error("Please enter a subject to generate an archive.")
progress(0.1, desc="🔍 Curator is analyzing your request...")
augmented_prompt = user_prompt.strip()
if is_monochromy:
augmented_prompt += f", monochromy in {monochromy_color} color"
if is_transparent:
augmented_prompt += ", transparent background"
final_prompt = augmented_prompt
try:
final_prompt = curator.refine_prompt(augmented_prompt)
except Exception:
pass
progress(0.3, desc="🎨 Generating historical archive... (1-2 min)")
result = engine.generate_from_text(final_prompt)
if "transparent" in augmented_prompt.lower():
try:
progress(0.8, desc="✂️ Removing background...")
from rembg import remove
result = remove(result)
except Exception as e:
print(f"Failed to remove background: {e}")
# Archive in background
threading.Thread(
target=archive_generation, args=(result, final_prompt), daemon=True
).start()
progress(1.0, desc="✅ Generated! Indexing in background...")
return result
except gr.Error:
raise
except Exception as e:
raise gr.Error(f"Generation failed: {str(e)}")
def ui_search(query):
"""Semantic search — calls memory directly."""
try:
if not query or not query.strip():
raise gr.Error("Please enter search keywords.")
emb = vision.embed_text(query)
if not emb:
return []
results = memory.search_by_vector(emb, k=9)
gallery_items = []
if results["ids"]:
for i, uid in enumerate(results["ids"][0]):
meta = results["metadatas"][0][i]
path = meta.get("path")
if path and os.path.exists(path):
gallery_items.append((path, meta.get("tags", "")))
if not gallery_items:
gr.Info("🔍 No artifacts found. Try different keywords.")
return gallery_items
except gr.Error:
raise
except Exception as e:
raise gr.Error(f"Search error: {str(e)}")
# ━━━ Build Gradio Interface ━━━━━━━━━━━━━━━━━━━━━━━━━━━
custom_css = """
button { transition: all 0.2s ease-in-out !important; }
button:hover { transform: scale(1.02); opacity: 0.95; }
button:active { transform: scale(0.98); }
button:focus-visible, textarea:focus-visible, input:focus-visible {
outline: 2px solid #8B5CF6 !important;
outline-offset: 2px !important;
}
@media (max-width: 768px) { .gallery { gap: 0.5rem !important; } }
"""
with gr.Blocks(
title="Cora — Historical Archive Generator",
theme=gr.themes.Monochrome(),
css=custom_css,
) as demo:
gr.Markdown(
f"""
# 🏛️ Cora — The Archive Curator
**Generate AI-powered historical illustrations** using SDXL and Visual RAG.
Transform simple prompts into detailed historical scenes, artifacts, and engravings.
**How it works**: Your prompt is enhanced by an AI curator, then rendered as a
historically-themed illustration and archived for future semantic search.
*Active Curator Backend:* **{{curator.backend}}** | *Model:* **{{curator.MODEL_ID}}**
"""
)
with gr.Tabs():
with gr.Tab("🎨 Generate"):
with gr.Row():
with gr.Column(scale=1):
txt_input = gr.Textbox(
label="Historical Subject",
placeholder="Describe a historical scene, artifact, or character...",
lines=3,
info="Be specific! Mention time periods, cultures, contexts, or ask for a monochromy.",
)
with gr.Row():
chk_monochromy = gr.Checkbox(label="Monochromy", value=False)
color_picker = gr.ColorPicker(label="Monochromy Color", value="#000000")
chk_transparent = gr.Checkbox(label="Transparent Background", value=False)
btn_generate = gr.Button(
"🔮 Generate Archive", variant="primary", size="lg"
)
gr.Examples(
examples=[
["A woman mending fishing nets by the shore of a medieval village"],
["Baking bread in a communal stone oven, soot and flour on hands"],
["An apprentice carving a wooden chair in a 17th-century workshop"],
["A Roman merchant counting copper coins at a busy market stall"],
["Tending to a small vegetable garden in the courtyard of a Tudor cottage"],
["Spinning wool into yarn using a simple wooden spindle by a hearth"],
],
inputs=txt_input,
label="💡 Daily Life Examples (click to use)",
)
with gr.Column(scale=1):
output_image = gr.Image(
label="🖼️ Recovered Artifact", show_label=True
)
btn_generate.click(
fn=ui_generate,
inputs=[txt_input, chk_monochromy, color_picker, chk_transparent],
outputs=output_image,
show_progress="full",
)
with gr.Tab("📚 The Archive"):
gr.Markdown(
"""
### Search the Visual Memory
Use semantic search to find archived artifacts by keywords, themes, or cultural markers.
"""
)
with gr.Row():
search_box = gr.Textbox(
label="Search Query",
placeholder="Try: 'roman armor', 'medieval', 'warrior'",
scale=4,
)
btn_search = gr.Button("🔍 Search", variant="secondary", scale=1)
gallery = gr.Gallery(
label="Found Artifacts",
columns=[1, 2, 3],
height="auto",
object_fit="contain",
show_label=True,
)
btn_search.click(fn=ui_search, inputs=[search_box], outputs=gallery)
gr.Examples(
examples=[
["roman gladiator"],
["medieval castle"],
["viking warrior"],
["renaissance art"],
],
inputs=search_box,
label="💡 Quick Search Terms",
)
# ━━━ Mount FastAPI into Gradio ━━━━━━━━━━━━━━━━━━━━━━━━
app = gr.mount_gradio_app(api, demo, path="/")
# ━━━ Launch ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
if __name__ == "__main__":
import uvicorn
port = int(os.environ.get("PORT", 7860))
uvicorn.run(app, host="0.0.0.0", port=port)