1730 lines
62 KiB
Python
1730 lines
62 KiB
Python
|
|
#!/usr/bin/env python3
|
||
|
|
"""
|
||
|
|
SuperDave AI 2.0 — FastAPI Backend Server
|
||
|
|
Orchestrates Pinokio models (Llama, Forge, Janus, Google AI)
|
||
|
|
Manages memory (ORACLE), web access, and vision capabilities
|
||
|
|
"""
|
||
|
|
|
||
|
|
import os
|
||
|
|
import json
|
||
|
|
import logging
|
||
|
|
import asyncio
|
||
|
|
import base64
|
||
|
|
import subprocess
|
||
|
|
import contextlib
|
||
|
|
from datetime import datetime, timedelta
|
||
|
|
from pathlib import Path
|
||
|
|
from typing import Optional, List, Dict, Any
|
||
|
|
|
||
|
|
from fastapi import FastAPI, HTTPException, Header, BackgroundTasks, WebSocket, WebSocketDisconnect, Body
|
||
|
|
from fastapi.responses import JSONResponse, FileResponse
|
||
|
|
from fastapi.middleware.cors import CORSMiddleware
|
||
|
|
from fastapi.staticfiles import StaticFiles
|
||
|
|
from pydantic import BaseModel, Field
|
||
|
|
import uvicorn
|
||
|
|
import psutil
|
||
|
|
import aiohttp
|
||
|
|
import requests
|
||
|
|
|
||
|
|
# Dual-layer symbolic integration
|
||
|
|
try:
|
||
|
|
from superdave.dual_layer_integration import integrate_with_server
|
||
|
|
DUAL_LAYER_ENABLED = True
|
||
|
|
except ImportError as e:
|
||
|
|
logger.warning(f"Dual-layer symbolic integration not available: {e}")
|
||
|
|
DUAL_LAYER_ENABLED = False
|
||
|
|
|
||
|
|
# Configure logging
|
||
|
|
logging.basicConfig(
|
||
|
|
level=logging.INFO,
|
||
|
|
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
||
|
|
)
|
||
|
|
logger = logging.getLogger(__name__)
|
||
|
|
|
||
|
|
# GPU inference
|
||
|
|
try:
|
||
|
|
import torch
|
||
|
|
CHAT_AVAILABLE = False
|
||
|
|
try:
|
||
|
|
from llama_cpp import Llama
|
||
|
|
CHAT_AVAILABLE = True
|
||
|
|
except ImportError:
|
||
|
|
logger.info("llama_cpp not available — chat will use Tabby API")
|
||
|
|
|
||
|
|
IMAGE_AVAILABLE = False
|
||
|
|
try:
|
||
|
|
from diffusers import AutoPipelineForText2Image
|
||
|
|
IMAGE_AVAILABLE = True
|
||
|
|
except ImportError:
|
||
|
|
logger.info("diffusers not available — image generation disabled")
|
||
|
|
|
||
|
|
GPU_AVAILABLE = CHAT_AVAILABLE or IMAGE_AVAILABLE
|
||
|
|
except ImportError as e:
|
||
|
|
logger.warning(f"torch not available: {e}")
|
||
|
|
GPU_AVAILABLE = False
|
||
|
|
CHAT_AVAILABLE = False
|
||
|
|
IMAGE_AVAILABLE = False
|
||
|
|
|
||
|
|
# Configuration
|
||
|
|
VRAM_WARNING = 6.5
|
||
|
|
VRAM_CRITICAL = 7.8
|
||
|
|
TOTAL_VRAM = 8.0
|
||
|
|
|
||
|
|
# VRAM Mode Configuration (GTX 1080 + 256GB RAM compressed-model strategy)
|
||
|
|
VRAM_CONFIGS = {
|
||
|
|
"8GB": {
|
||
|
|
"max_memory": {"0": "6GiB", "cpu": "16GiB"},
|
||
|
|
"steps_cap": 4,
|
||
|
|
"guidance_cap": 1.0,
|
||
|
|
"enable_janus": False,
|
||
|
|
"enable_local_llm": False,
|
||
|
|
"multi_gpu": False,
|
||
|
|
"description": "CPU offload mode - safe for 8GB VRAM"
|
||
|
|
},
|
||
|
|
"24GB": {
|
||
|
|
"max_memory": {"0": "22GiB", "cpu": "64GiB"},
|
||
|
|
"steps_cap": 20,
|
||
|
|
"guidance_cap": 7.0,
|
||
|
|
"enable_janus": True,
|
||
|
|
"enable_local_llm": True,
|
||
|
|
"multi_gpu": False,
|
||
|
|
"description": "Full GPU + unified memory - Janus & LLM enabled"
|
||
|
|
},
|
||
|
|
"48GB": {
|
||
|
|
"max_memory": {"0": "22GiB", "1": "22GiB", "cpu": "128GiB"},
|
||
|
|
"steps_cap": 30,
|
||
|
|
"guidance_cap": 10.0,
|
||
|
|
"enable_janus": True,
|
||
|
|
"enable_local_llm": True,
|
||
|
|
"multi_gpu": True,
|
||
|
|
"description": "Multi-GPU + large unified memory - max capacity"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
CURRENT_VRAM_MODE = "8GB"
|
||
|
|
|
||
|
|
# GPU Inference via Tabby (CUDA-accelerated inference server)
|
||
|
|
TABBY_API = os.getenv("TABBY_API", "http://192.168.2.12:11436")
|
||
|
|
|
||
|
|
# FedMart external endpoint (for telemetry ingestion)
|
||
|
|
FEDMART_ENDPOINT = os.getenv("FEDMART_ENDPOINT", "http://localhost:8000/fedmart/ingest/xic")
|
||
|
|
|
||
|
|
# Fallback: local diffusers for images (if GPU available)
|
||
|
|
_image_pipe = None
|
||
|
|
IMAGE_MODEL_PATH = "/mnt/w/SuperDave/models/sdxl-turbo"
|
||
|
|
|
||
|
|
# Rate limiting for image generation
|
||
|
|
_image_generation_lock = asyncio.Lock()
|
||
|
|
_image_generation_active = False
|
||
|
|
|
||
|
|
def get_image_pipe():
|
||
|
|
"""Lazy-load image pipeline on first use"""
|
||
|
|
if not IMAGE_AVAILABLE:
|
||
|
|
raise RuntimeError("diffusers not installed")
|
||
|
|
global _image_pipe
|
||
|
|
if _image_pipe is None:
|
||
|
|
logger.info(f"Loading image pipeline from {IMAGE_MODEL_PATH}...")
|
||
|
|
_image_pipe = AutoPipelineForText2Image.from_pretrained(
|
||
|
|
IMAGE_MODEL_PATH, torch_dtype=torch.float16, variant="fp16",
|
||
|
|
device_map="balanced", max_memory={0: "6GiB", "cpu": "16GiB"}
|
||
|
|
)
|
||
|
|
_image_pipe.enable_attention_slicing()
|
||
|
|
_image_pipe.vae.enable_slicing()
|
||
|
|
logger.info("Image pipeline loaded successfully")
|
||
|
|
return _image_pipe
|
||
|
|
|
||
|
|
@contextlib.asynccontextmanager
|
||
|
|
async def lifespan(app: FastAPI):
|
||
|
|
logger.info("🚀 SuperDave AI 2.0 starting up...")
|
||
|
|
logger.info(f"VRAM limits: Warning={VRAM_WARNING}GB, Critical={VRAM_CRITICAL}GB")
|
||
|
|
logger.info(f"LLM inference: Tabby API at {TABBY_API}")
|
||
|
|
if IMAGE_AVAILABLE:
|
||
|
|
logger.info(f"Image generation: enabled (diffusers + SDXL-Turbo)")
|
||
|
|
# Pre-load pipeline on startup to eliminate first-request delay
|
||
|
|
# TEMPORARILY DISABLED due to loading issues
|
||
|
|
# try:
|
||
|
|
# logger.info("Pre-loading image pipeline...")
|
||
|
|
# _ = get_image_pipe()
|
||
|
|
# logger.info("✅ Image pipeline pre-loaded successfully")
|
||
|
|
# except Exception as e:
|
||
|
|
# logger.warning(f"Could not pre-load image pipeline: {e}")
|
||
|
|
elif GPU_AVAILABLE:
|
||
|
|
logger.warning("Image generation: disabled (torch/diffusers not installed)")
|
||
|
|
yield
|
||
|
|
logger.info("🛑 SuperDave AI 2.0 shutting down...")
|
||
|
|
|
||
|
|
app = FastAPI(
|
||
|
|
title="SuperDave AI 2.0",
|
||
|
|
description="Multi-modal AI system with autonomous memory and web access",
|
||
|
|
version="2.0.0",
|
||
|
|
lifespan=lifespan
|
||
|
|
)
|
||
|
|
|
||
|
|
# Enable CORS
|
||
|
|
app.add_middleware(
|
||
|
|
CORSMiddleware,
|
||
|
|
allow_origins=["*"],
|
||
|
|
allow_credentials=True,
|
||
|
|
allow_methods=["*"],
|
||
|
|
allow_headers=["*"],
|
||
|
|
)
|
||
|
|
|
||
|
|
# Serve FedMart UI static files
|
||
|
|
import os
|
||
|
|
fedmart_ui_path = os.path.join(os.path.dirname(__file__), "superdave/fedmart_ui/modules/xic_panel")
|
||
|
|
fedmart_ui_base = os.path.join(os.path.dirname(__file__), "superdave/fedmart_ui")
|
||
|
|
if os.path.exists(fedmart_ui_path):
|
||
|
|
app.mount("/ui", StaticFiles(directory=fedmart_ui_path, html=True), name="ui")
|
||
|
|
logger.info(f"Mounted FedMart UI at /ui from {fedmart_ui_path}")
|
||
|
|
elif os.path.exists(fedmart_ui_base):
|
||
|
|
app.mount("/ui", StaticFiles(directory=fedmart_ui_base, html=True), name="ui")
|
||
|
|
logger.info(f"Mounted FedMart UI at /ui from {fedmart_ui_base}")
|
||
|
|
|
||
|
|
# Serve Glyph Dashboard
|
||
|
|
glyph_dashboard_path = os.path.join(os.path.dirname(__file__), "superdave/glyph_dashboard")
|
||
|
|
if os.path.exists(glyph_dashboard_path):
|
||
|
|
app.mount("/glyphs", StaticFiles(directory=glyph_dashboard_path, html=True), name="glyphs")
|
||
|
|
logger.info(f"Mounted Glyph Dashboard at /glyphs from {glyph_dashboard_path}")
|
||
|
|
|
||
|
|
GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY", "")
|
||
|
|
OUTPUT_DIR = Path("C:\\SuperDave_Projects\\outputs") if os.name == "nt" else Path("/tmp/superdave_outputs")
|
||
|
|
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
||
|
|
# Serve generated outputs for browser preview
|
||
|
|
app.mount("/outputs", StaticFiles(directory=str(OUTPUT_DIR)), name="outputs")
|
||
|
|
|
||
|
|
# Dual-layer symbolic integration
|
||
|
|
if DUAL_LAYER_ENABLED:
|
||
|
|
try:
|
||
|
|
integrate_with_server(app)
|
||
|
|
logger.info("✅ Dual-layer symbolic system integrated (glyphs + resonance)")
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"Failed to integrate dual-layer system: {e}")
|
||
|
|
|
||
|
|
# Memory (ORACLE) system
|
||
|
|
MEMORY_FILE = OUTPUT_DIR.parent / "memory.json"
|
||
|
|
MEMORY_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||
|
|
|
||
|
|
class OracleMemory:
|
||
|
|
"""Autonomous memory system for SuperDave"""
|
||
|
|
|
||
|
|
def __init__(self, memory_path: Path = MEMORY_FILE):
|
||
|
|
self.path = memory_path
|
||
|
|
self.memory = self._load()
|
||
|
|
|
||
|
|
def _load(self) -> Dict:
|
||
|
|
"""Load memory from disk"""
|
||
|
|
if self.path.exists():
|
||
|
|
try:
|
||
|
|
with open(self.path, 'r') as f:
|
||
|
|
return json.load(f)
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"Failed to load memory: {e}")
|
||
|
|
return {"facts": {}, "preferences": {}, "sessions": {}}
|
||
|
|
|
||
|
|
def _save(self):
|
||
|
|
"""Save memory to disk"""
|
||
|
|
try:
|
||
|
|
with open(self.path, 'w') as f:
|
||
|
|
json.dump(self.memory, f, indent=2, default=str)
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"Failed to save memory: {e}")
|
||
|
|
|
||
|
|
def remember(self, key: str, value: Any, category: str = "facts"):
|
||
|
|
"""Store a fact or preference"""
|
||
|
|
if category not in self.memory:
|
||
|
|
self.memory[category] = {}
|
||
|
|
self.memory[category][key] = {
|
||
|
|
"value": value,
|
||
|
|
"timestamp": datetime.now().isoformat()
|
||
|
|
}
|
||
|
|
self._save()
|
||
|
|
logger.info(f"Remembered: {key} = {value}")
|
||
|
|
|
||
|
|
def recall(self, key: str, category: str = "facts") -> Optional[Any]:
|
||
|
|
"""Retrieve a stored fact"""
|
||
|
|
if category in self.memory and key in self.memory[category]:
|
||
|
|
return self.memory[category][key]["value"]
|
||
|
|
return None
|
||
|
|
|
||
|
|
def forget(self, key: str, category: str = "facts"):
|
||
|
|
"""Remove a stored fact"""
|
||
|
|
if category in self.memory and key in self.memory[category]:
|
||
|
|
del self.memory[category][key]
|
||
|
|
self._save()
|
||
|
|
logger.info(f"Forgot: {key}")
|
||
|
|
|
||
|
|
def session_log(self, user_id: str, action: str, details: Dict = None):
|
||
|
|
"""Log user action for tracking"""
|
||
|
|
if user_id not in self.memory["sessions"]:
|
||
|
|
self.memory["sessions"][user_id] = []
|
||
|
|
self.memory["sessions"][user_id].append({
|
||
|
|
"action": action,
|
||
|
|
"timestamp": datetime.now().isoformat(),
|
||
|
|
"details": details or {}
|
||
|
|
})
|
||
|
|
self._save()
|
||
|
|
|
||
|
|
oracle = OracleMemory()
|
||
|
|
|
||
|
|
|
||
|
|
# User usage tracking
|
||
|
|
_user_usage = {}
|
||
|
|
|
||
|
|
def track_user_usage(user_id: str, action: str):
|
||
|
|
"""Track user actions for usage analytics"""
|
||
|
|
if user_id not in _user_usage:
|
||
|
|
_user_usage[user_id] = {
|
||
|
|
"image_generations": 0,
|
||
|
|
"chat_messages": 0,
|
||
|
|
"vision_analyses": 0,
|
||
|
|
"video_generations": 0,
|
||
|
|
"first_seen": datetime.now().isoformat(),
|
||
|
|
"last_seen": datetime.now().isoformat()
|
||
|
|
}
|
||
|
|
|
||
|
|
if action == "image_gen":
|
||
|
|
_user_usage[user_id]["image_generations"] += 1
|
||
|
|
elif action == "chat":
|
||
|
|
_user_usage[user_id]["chat_messages"] += 1
|
||
|
|
elif action == "vision":
|
||
|
|
_user_usage[user_id]["vision_analyses"] += 1
|
||
|
|
elif action == "video":
|
||
|
|
_user_usage[user_id]["video_generations"] += 1
|
||
|
|
|
||
|
|
_user_usage[user_id]["last_seen"] = datetime.now().isoformat()
|
||
|
|
|
||
|
|
|
||
|
|
@app.get("/api/user-usage")
|
||
|
|
async def get_user_usage(authorization: Optional[str] = Header(None)):
|
||
|
|
"""Get user usage statistics"""
|
||
|
|
user_id = authorization.replace("Bearer ", "") if authorization else "anonymous"
|
||
|
|
track_user_usage(user_id, "usage_check")
|
||
|
|
|
||
|
|
user_data = _user_usage.get(user_id, {
|
||
|
|
"image_generations": 0,
|
||
|
|
"chat_messages": 0,
|
||
|
|
"vision_analyses": 0,
|
||
|
|
"video_generations": 0,
|
||
|
|
"first_seen": datetime.now().isoformat(),
|
||
|
|
"last_seen": datetime.now().isoformat()
|
||
|
|
})
|
||
|
|
|
||
|
|
return {"status": "success", "user_id": user_id, "usage": user_data}
|
||
|
|
|
||
|
|
|
||
|
|
# ========================
|
||
|
|
# VRAM Management
|
||
|
|
# ========================
|
||
|
|
|
||
|
|
def get_vram_usage() -> Dict[str, float]:
|
||
|
|
"""Get current VRAM usage"""
|
||
|
|
try:
|
||
|
|
# Try NVIDIA GPU memory (nvidia-smi)
|
||
|
|
result = subprocess.run(
|
||
|
|
["nvidia-smi", "--query-gpu=memory.used,memory.total", "--format=csv,nounits,noheader"],
|
||
|
|
capture_output=True, text=True, timeout=5
|
||
|
|
)
|
||
|
|
if result.returncode == 0:
|
||
|
|
used, total = result.stdout.strip().split(',')
|
||
|
|
used_gb = float(used) / 1024
|
||
|
|
total_gb = float(total) / 1024
|
||
|
|
return {
|
||
|
|
"used_gb": round(used_gb, 2),
|
||
|
|
"total_gb": round(total_gb, 2),
|
||
|
|
"percent": round(used_gb / total_gb * 100, 2)
|
||
|
|
}
|
||
|
|
except Exception as e:
|
||
|
|
logger.warning(f"nvidia-smi failed: {e}, using fallback")
|
||
|
|
|
||
|
|
# Fallback: system RAM (not ideal but better than nothing)
|
||
|
|
mem = psutil.virtual_memory()
|
||
|
|
return {
|
||
|
|
"used_gb": round(mem.used / 1e9, 2),
|
||
|
|
"total_gb": round(mem.total / 1e9, 2),
|
||
|
|
"percent": mem.percent
|
||
|
|
}
|
||
|
|
|
||
|
|
def check_vram_conflict(model1: str, model2: str) -> bool:
|
||
|
|
"""Check if two models can run simultaneously (Forge + Janus conflict)"""
|
||
|
|
conflict_pairs = [("forge", "janus"), ("janus", "forge")]
|
||
|
|
return (model1, model2) in conflict_pairs
|
||
|
|
|
||
|
|
# ========================
|
||
|
|
# Web Access & Scraping
|
||
|
|
# ========================
|
||
|
|
|
||
|
|
async def fetch_url(url: str, timeout: int = 10) -> Dict[str, Any]:
|
||
|
|
"""Fetch and parse web content"""
|
||
|
|
try:
|
||
|
|
async with aiohttp.ClientSession() as session:
|
||
|
|
async with session.get(url, timeout=timeout) as resp:
|
||
|
|
if resp.status == 200:
|
||
|
|
text = await resp.text()
|
||
|
|
# Basic HTML to markdown (can be enhanced)
|
||
|
|
return {
|
||
|
|
"status": "success",
|
||
|
|
"url": url,
|
||
|
|
"content": text[:5000], # First 5k chars
|
||
|
|
"content_type": resp.content_type
|
||
|
|
}
|
||
|
|
except Exception as e:
|
||
|
|
return {
|
||
|
|
"status": "error",
|
||
|
|
"url": url,
|
||
|
|
"error": str(e)
|
||
|
|
}
|
||
|
|
|
||
|
|
# ========================
|
||
|
|
# Pinokio Connectors
|
||
|
|
# ========================
|
||
|
|
|
||
|
|
class LlamaConnector:
|
||
|
|
"""Chat via Tabby API (CUDA-accelerated on GPU)"""
|
||
|
|
|
||
|
|
@staticmethod
|
||
|
|
async def chat(messages: List[Dict], model: str = "llama-3.5-35b",
|
||
|
|
temperature: float = 0.7, top_p: float = 0.9,
|
||
|
|
user_id: str = "anonymous") -> Dict:
|
||
|
|
"""Run chat via Tabby API (GPU inference)"""
|
||
|
|
try:
|
||
|
|
endpoint = f"{TABBY_API}/v1/chat/completions"
|
||
|
|
payload = {
|
||
|
|
"model": model,
|
||
|
|
"messages": messages,
|
||
|
|
"temperature": temperature,
|
||
|
|
"top_p": top_p,
|
||
|
|
"max_tokens": 2000,
|
||
|
|
}
|
||
|
|
|
||
|
|
response = requests.post(endpoint, json=payload, timeout=300)
|
||
|
|
result = response.json() if response.status_code == 200 else {"status": "error", "message": f"HTTP {response.status_code}"}
|
||
|
|
|
||
|
|
if "error" not in result and "status" not in result:
|
||
|
|
oracle.session_log(user_id, "chat", {"messages_count": len(messages)})
|
||
|
|
logger.info(f"Chat successful: {len(messages)} messages via Tabby")
|
||
|
|
else:
|
||
|
|
logger.warning(f"Chat error: {result}")
|
||
|
|
|
||
|
|
return result
|
||
|
|
except requests.ConnectionError:
|
||
|
|
return {"status": "error", "message": f"Cannot connect to Tabby at {TABBY_API}. Is it running?"}
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"Chat error: {e}")
|
||
|
|
return {"status": "error", "message": str(e)}
|
||
|
|
|
||
|
|
|
||
|
|
class ForgeConnector:
|
||
|
|
"""SDXL-Turbo image generation via diffusers (GPU-accelerated)"""
|
||
|
|
|
||
|
|
@staticmethod
|
||
|
|
async def generate(prompt: str, width: int = 768, height: int = 768,
|
||
|
|
steps: int = 4, negative_prompt: str = "",
|
||
|
|
guidance_scale: float = 0.0, user_id: str = "anonymous") -> Dict:
|
||
|
|
"""Generate image via SDXL-Turbo on GPU"""
|
||
|
|
|
||
|
|
global _image_generation_active
|
||
|
|
|
||
|
|
# Rate limiting: prevent concurrent image generation
|
||
|
|
if _image_generation_active:
|
||
|
|
return {"status": "error", "message": "Image generation already in progress - please wait"}
|
||
|
|
|
||
|
|
async with _image_generation_lock:
|
||
|
|
_image_generation_active = True
|
||
|
|
try:
|
||
|
|
vram = get_vram_usage()
|
||
|
|
if vram["used_gb"] > VRAM_CRITICAL:
|
||
|
|
return {"status": "error", "message": "VRAM critical - close other models"}
|
||
|
|
|
||
|
|
pipe = get_image_pipe()
|
||
|
|
image = pipe(
|
||
|
|
prompt=prompt,
|
||
|
|
negative_prompt=negative_prompt or None,
|
||
|
|
num_inference_steps=steps,
|
||
|
|
guidance_scale=guidance_scale,
|
||
|
|
width=width,
|
||
|
|
height=height,
|
||
|
|
).images[0]
|
||
|
|
|
||
|
|
out_path = OUTPUT_DIR / f"image_{datetime.now().strftime('%Y%m%d_%H%M%S')}.png"
|
||
|
|
image.save(out_path)
|
||
|
|
|
||
|
|
oracle.session_log(user_id, "image_gen", {"prompt": prompt[:50], "resolution": f"{width}x{height}"})
|
||
|
|
|
||
|
|
# Track user usage
|
||
|
|
track_user_usage(user_id, "image_gen")
|
||
|
|
|
||
|
|
# Free VRAM after generation
|
||
|
|
import gc
|
||
|
|
del image
|
||
|
|
gc.collect()
|
||
|
|
if torch.cuda.is_available():
|
||
|
|
torch.cuda.empty_cache()
|
||
|
|
|
||
|
|
url_path = f"/outputs/{out_path.name}"
|
||
|
|
return {"status": "success", "image_path": url_path}
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"Image generation error: {e}")
|
||
|
|
return {"status": "error", "message": str(e)}
|
||
|
|
finally:
|
||
|
|
_image_generation_active = False
|
||
|
|
|
||
|
|
|
||
|
|
class JanusConnector:
|
||
|
|
"""Janus video generation via Pinokio or local fallback"""
|
||
|
|
|
||
|
|
_generation_lock = asyncio.Lock()
|
||
|
|
_generation_active = False
|
||
|
|
_janus_api_url: Optional[str] = None
|
||
|
|
|
||
|
|
@classmethod
|
||
|
|
def configure(cls, api_url: Optional[str] = None) -> None:
|
||
|
|
cls._janus_api_url = api_url
|
||
|
|
|
||
|
|
@classmethod
|
||
|
|
async def generate(cls, prompt: str, duration: float = 5.0, fps: int = 30,
|
||
|
|
width: int = 512, height: int = 512, user_id: str = "anonymous") -> Dict:
|
||
|
|
"""Generate video from prompt using Janus-Pro-7B or local fallback."""
|
||
|
|
if cls._generation_active:
|
||
|
|
return {"status": "error", "message": "Video generation already in progress"}
|
||
|
|
|
||
|
|
async with cls._generation_lock:
|
||
|
|
cls._generation_active = True
|
||
|
|
try:
|
||
|
|
output_dir = Path(OUTPUT_DIR) / "videos"
|
||
|
|
output_dir.mkdir(parents=True, exist_ok=True)
|
||
|
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||
|
|
safe_name = "".join(c if c.isalnum() or c in " _-" else "_" for c in prompt[:40])
|
||
|
|
video_path = output_dir / f"janus_{timestamp}_{safe_name}.mp4"
|
||
|
|
|
||
|
|
# Try Pinokio Janus API first
|
||
|
|
if cls._janus_api_url:
|
||
|
|
try:
|
||
|
|
async with aiohttp.ClientSession() as session:
|
||
|
|
payload = {
|
||
|
|
"prompt": prompt,
|
||
|
|
"duration": duration,
|
||
|
|
"fps": fps,
|
||
|
|
"width": width,
|
||
|
|
"height": height,
|
||
|
|
}
|
||
|
|
async with session.post(cls._janus_api_url, json=payload, timeout=120) as resp:
|
||
|
|
if resp.status == 200:
|
||
|
|
data = await resp.json()
|
||
|
|
if data.get("video_path"):
|
||
|
|
logger.info(f"[JANUS] Video generated via Pinokio: {data['video_path']}")
|
||
|
|
return {"status": "success", "video_path": data["video_path"]}
|
||
|
|
logger.warning(f"[JANUS] Pinokio API returned {resp.status}, falling back")
|
||
|
|
except Exception as e:
|
||
|
|
logger.warning(f"[JANUS] Pinokio API error: {e}, falling back to local generation")
|
||
|
|
|
||
|
|
# Local fallback: generate frames with Pillow, assemble with available tools
|
||
|
|
import numpy as np
|
||
|
|
try:
|
||
|
|
from PIL import Image, ImageDraw, ImageFont
|
||
|
|
except ImportError:
|
||
|
|
return {"status": "error", "message": "Pillow not available for local video generation"}
|
||
|
|
|
||
|
|
total_frames = int(duration * fps)
|
||
|
|
frame_interval = 1.0 / fps
|
||
|
|
frames: List[Image.Image] = []
|
||
|
|
|
||
|
|
logger.info(f"[JANUS] Generating {total_frames} frames locally ({duration}s @ {fps}fps)...")
|
||
|
|
|
||
|
|
for i in range(total_frames):
|
||
|
|
t = i * frame_interval
|
||
|
|
frame = Image.new("RGB", (width, height), (20, 20, 30))
|
||
|
|
draw = ImageDraw.Draw(frame)
|
||
|
|
|
||
|
|
# Animated gradient background
|
||
|
|
phase = (t / duration) * 2 * 3.14159
|
||
|
|
r = int(128 + 64 * np.sin(phase))
|
||
|
|
g = int(64 + 32 * np.cos(phase * 0.7))
|
||
|
|
b = int(128 + 64 * np.sin(phase * 1.3))
|
||
|
|
for y in range(height):
|
||
|
|
y_norm = y / height
|
||
|
|
color = (int(r * (1 - y_norm * 0.5)), int(g * (1 - y_norm * 0.3)), int(b * (1 - y_norm * 0.4)))
|
||
|
|
draw.line([(0, y), (width, y)], fill=color)
|
||
|
|
|
||
|
|
# Draw prompt text overlay
|
||
|
|
lines = []
|
||
|
|
words = prompt.split()
|
||
|
|
line = ""
|
||
|
|
for word in words:
|
||
|
|
test = line + " " + word if line else word
|
||
|
|
if len(test) < 50:
|
||
|
|
line = test
|
||
|
|
else:
|
||
|
|
lines.append(line)
|
||
|
|
line = word
|
||
|
|
if line:
|
||
|
|
lines.append(line)
|
||
|
|
|
||
|
|
font_size = max(16, min(36, width // 20))
|
||
|
|
try:
|
||
|
|
font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", font_size)
|
||
|
|
except (OSError, IOError):
|
||
|
|
font = ImageFont.load_default()
|
||
|
|
|
||
|
|
text_y = height // 4
|
||
|
|
for line_text in lines:
|
||
|
|
bbox = draw.textbbox((0, 0), line_text, font=font)
|
||
|
|
tw = bbox[2] - bbox[0]
|
||
|
|
th = bbox[3] - bbox[1]
|
||
|
|
draw.text(((width - tw) // 2, text_y), line_text, fill=(255, 255, 255), font=font)
|
||
|
|
text_y += th + 8
|
||
|
|
|
||
|
|
# Frame counter
|
||
|
|
counter_text = f"Frame {i+1}/{total_frames} | {t:.1f}s"
|
||
|
|
draw.text((10, height - 30), counter_text, fill=(180, 180, 180), font=font)
|
||
|
|
|
||
|
|
frames.append(frame)
|
||
|
|
|
||
|
|
# Try to assemble with available tools
|
||
|
|
assembled = False
|
||
|
|
|
||
|
|
# Try ffmpeg
|
||
|
|
import subprocess
|
||
|
|
try:
|
||
|
|
import tempfile
|
||
|
|
frame_dir = Path(tempfile.mkdtemp())
|
||
|
|
for i, frame in enumerate(frames):
|
||
|
|
frame.save(frame_dir / f"frame_{i:06d}.png")
|
||
|
|
frame_pattern = str(frame_dir / "frame_%06d.png")
|
||
|
|
result = subprocess.run(
|
||
|
|
["ffmpeg", "-y", "-framerate", str(fps), "-i", frame_pattern,
|
||
|
|
"-c:v", "libx264", "-pix_fmt", "yuv420p", str(video_path)],
|
||
|
|
capture_output=True, text=True, timeout=120
|
||
|
|
)
|
||
|
|
if result.returncode == 0 and video_path.exists():
|
||
|
|
assembled = True
|
||
|
|
logger.info(f"[JANUS] Video assembled with ffmpeg: {video_path}")
|
||
|
|
import shutil
|
||
|
|
shutil.rmtree(frame_dir, ignore_errors=True)
|
||
|
|
except (subprocess.TimeoutExpired, FileNotFoundError, Exception) as e:
|
||
|
|
logger.warning(f"[JANUS] ffmpeg assembly failed: {e}")
|
||
|
|
|
||
|
|
if not assembled:
|
||
|
|
# Save as animated GIF instead
|
||
|
|
gif_path = video_path.with_suffix(".gif")
|
||
|
|
frames[0].save(
|
||
|
|
gif_path, save_all=True, append_images=frames[1:],
|
||
|
|
duration=int(frame_interval * 1000), loop=0, optimize=False
|
||
|
|
)
|
||
|
|
video_path = gif_path
|
||
|
|
assembled = True
|
||
|
|
logger.info(f"[JANUS] Saved as animated GIF: {gif_path}")
|
||
|
|
|
||
|
|
if not assembled or not video_path.exists():
|
||
|
|
return {"status": "error", "message": "Failed to assemble video frames"}
|
||
|
|
|
||
|
|
file_size = video_path.stat().st_size
|
||
|
|
url_path = f"/outputs/videos/{video_path.name}"
|
||
|
|
logger.info(f"[JANUS] Video generated: {video_path} ({file_size} bytes)")
|
||
|
|
|
||
|
|
# Log to oracle if available
|
||
|
|
try:
|
||
|
|
oracle.session_log(user_id, "video_generation", {"prompt": prompt[:80], "path": str(video_path), "size": file_size})
|
||
|
|
except Exception:
|
||
|
|
pass
|
||
|
|
|
||
|
|
return {
|
||
|
|
"status": "success",
|
||
|
|
"video_path": url_path,
|
||
|
|
"file_size": file_size,
|
||
|
|
"duration": duration,
|
||
|
|
"frames": total_frames,
|
||
|
|
"method": "local_fallback" if not cls._janus_api_url else "pinokio_api"
|
||
|
|
}
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"[JANUS] Video generation error: {e}")
|
||
|
|
return {"status": "error", "message": str(e)}
|
||
|
|
finally:
|
||
|
|
cls._generation_active = False
|
||
|
|
|
||
|
|
|
||
|
|
class GoogleAIConnector:
|
||
|
|
"""Google Gemini vision API"""
|
||
|
|
|
||
|
|
@staticmethod
|
||
|
|
async def analyze(image_path: str, prompt: str = "Analyze this image in detail",
|
||
|
|
user_id: str = "anonymous") -> Dict:
|
||
|
|
"""Analyze image with Google Gemini"""
|
||
|
|
|
||
|
|
if not GOOGLE_API_KEY:
|
||
|
|
return {"status": "error", "message": "Google API key not configured"}
|
||
|
|
|
||
|
|
try:
|
||
|
|
import google.generativeai as genai
|
||
|
|
genai.configure(api_key=GOOGLE_API_KEY)
|
||
|
|
model = genai.GenerativeModel('gemini-1.5-pro-vision')
|
||
|
|
|
||
|
|
# Load image
|
||
|
|
with open(image_path, 'rb') as f:
|
||
|
|
image_data = base64.standard_b64encode(f.read()).decode('utf-8')
|
||
|
|
|
||
|
|
response = model.generate_content([
|
||
|
|
prompt,
|
||
|
|
{
|
||
|
|
"mime_type": "image/jpeg",
|
||
|
|
"data": image_data
|
||
|
|
}
|
||
|
|
])
|
||
|
|
|
||
|
|
oracle.session_log(user_id, "vision", {"image": image_path, "prompt": prompt[:50]})
|
||
|
|
|
||
|
|
return {
|
||
|
|
"status": "success",
|
||
|
|
"analysis": response.text
|
||
|
|
}
|
||
|
|
except ImportError:
|
||
|
|
return {"status": "error", "message": "google-generativeai not installed"}
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"Google AI error: {e}")
|
||
|
|
return {"status": "error", "message": str(e)}
|
||
|
|
|
||
|
|
# ========================
|
||
|
|
# FedMart Telemetry Integration
|
||
|
|
# ========================
|
||
|
|
|
||
|
|
class BroadcastManager:
|
||
|
|
"""Manages WebSocket connections for telemetry broadcasting"""
|
||
|
|
def __init__(self):
|
||
|
|
self.active_connections: List[WebSocket] = []
|
||
|
|
|
||
|
|
async def connect(self, websocket: WebSocket):
|
||
|
|
await websocket.accept()
|
||
|
|
self.active_connections.append(websocket)
|
||
|
|
logger.info(f"[FEDMART] Client connected. Total: {len(self.active_connections)}")
|
||
|
|
|
||
|
|
def disconnect(self, websocket: WebSocket):
|
||
|
|
self.active_connections.remove(websocket)
|
||
|
|
logger.info(f"[FEDMART] Client disconnected. Total: {len(self.active_connections)}")
|
||
|
|
|
||
|
|
async def broadcast(self, message: Dict):
|
||
|
|
"""Broadcast message to all connected clients"""
|
||
|
|
for connection in self.active_connections:
|
||
|
|
try:
|
||
|
|
await connection.send_json(message)
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"[FEDMART] Broadcast error: {e}")
|
||
|
|
|
||
|
|
broadcast_manager = BroadcastManager()
|
||
|
|
telemetry_buffer: List[Dict] = []
|
||
|
|
max_buffer_size = 1000
|
||
|
|
|
||
|
|
|
||
|
|
class PipelineControl:
|
||
|
|
"""Manages active XIC pipeline runs with real pause/throttle control."""
|
||
|
|
|
||
|
|
def __init__(self):
|
||
|
|
self._runs: Dict[str, Dict[str, Any]] = {}
|
||
|
|
self._pause_events: Dict[str, asyncio.Event] = {}
|
||
|
|
self._throttle_factors: Dict[str, float] = {}
|
||
|
|
self._lock = asyncio.Lock()
|
||
|
|
|
||
|
|
async def register_run(self, run_id: str) -> None:
|
||
|
|
"""Register a new pipeline run."""
|
||
|
|
async with self._lock:
|
||
|
|
self._runs[run_id] = {
|
||
|
|
"status": "running",
|
||
|
|
"created_at": datetime.now().isoformat(),
|
||
|
|
"paused_at": None,
|
||
|
|
"resumed_at": None,
|
||
|
|
"throttle_factor": 1.0,
|
||
|
|
"glyph_count": 0,
|
||
|
|
"steps_executed": 0,
|
||
|
|
}
|
||
|
|
self._pause_events[run_id] = asyncio.Event()
|
||
|
|
self._pause_events[run_id].set() # Not paused initially
|
||
|
|
self._throttle_factors[run_id] = 1.0
|
||
|
|
logger.info(f"[PIPELINE-CONTROL] Run {run_id} registered")
|
||
|
|
|
||
|
|
async def unregister_run(self, run_id: str) -> None:
|
||
|
|
"""Unregister a completed/failed run."""
|
||
|
|
async with self._lock:
|
||
|
|
self._runs.pop(run_id, None)
|
||
|
|
self._pause_events.pop(run_id, None)
|
||
|
|
self._throttle_factors.pop(run_id, None)
|
||
|
|
logger.info(f"[PIPELINE-CONTROL] Run {run_id} unregistered")
|
||
|
|
|
||
|
|
async def pause(self, run_id: str) -> bool:
|
||
|
|
"""Pause a run. Returns True if run exists and was paused."""
|
||
|
|
async with self._lock:
|
||
|
|
event = self._pause_events.get(run_id)
|
||
|
|
run = self._runs.get(run_id)
|
||
|
|
if event is None or run is None:
|
||
|
|
return False
|
||
|
|
event.clear()
|
||
|
|
run["status"] = "paused"
|
||
|
|
run["paused_at"] = datetime.now().isoformat()
|
||
|
|
logger.info(f"[PIPELINE-CONTROL] Run {run_id} paused")
|
||
|
|
await broadcast_manager.broadcast({
|
||
|
|
"type": "pipeline_control",
|
||
|
|
"action": "pause",
|
||
|
|
"run_id": run_id,
|
||
|
|
"timestamp": run["paused_at"]
|
||
|
|
})
|
||
|
|
return True
|
||
|
|
|
||
|
|
async def resume(self, run_id: str) -> bool:
|
||
|
|
"""Resume a paused run."""
|
||
|
|
async with self._lock:
|
||
|
|
event = self._pause_events.get(run_id)
|
||
|
|
run = self._runs.get(run_id)
|
||
|
|
if event is None or run is None:
|
||
|
|
return False
|
||
|
|
event.set()
|
||
|
|
run["status"] = "running"
|
||
|
|
run["resumed_at"] = datetime.now().isoformat()
|
||
|
|
logger.info(f"[PIPELINE-CONTROL] Run {run_id} resumed")
|
||
|
|
await broadcast_manager.broadcast({
|
||
|
|
"type": "pipeline_control",
|
||
|
|
"action": "resume",
|
||
|
|
"run_id": run_id,
|
||
|
|
"timestamp": run["resumed_at"]
|
||
|
|
})
|
||
|
|
return True
|
||
|
|
|
||
|
|
async def throttle(self, run_id: str, factor: float) -> bool:
|
||
|
|
"""Set throttle factor for a run (0.0 = stopped, 1.0 = full speed)."""
|
||
|
|
async with self._lock:
|
||
|
|
run = self._runs.get(run_id)
|
||
|
|
if run is None:
|
||
|
|
return False
|
||
|
|
factor = max(0.0, min(1.0, factor))
|
||
|
|
self._throttle_factors[run_id] = factor
|
||
|
|
run["throttle_factor"] = factor
|
||
|
|
run["status"] = "throttled" if factor < 1.0 else "running"
|
||
|
|
logger.info(f"[PIPELINE-CONTROL] Run {run_id} throttled to {factor:.1%}")
|
||
|
|
await broadcast_manager.broadcast({
|
||
|
|
"type": "pipeline_control",
|
||
|
|
"action": "throttle",
|
||
|
|
"run_id": run_id,
|
||
|
|
"factor": factor,
|
||
|
|
"timestamp": datetime.now().isoformat()
|
||
|
|
})
|
||
|
|
return True
|
||
|
|
|
||
|
|
async def wait_if_paused(self, run_id: str) -> None:
|
||
|
|
"""Block caller until run is resumed. Call this from pipeline execution loops."""
|
||
|
|
event = self._pause_events.get(run_id)
|
||
|
|
if event is not None:
|
||
|
|
await event.wait()
|
||
|
|
|
||
|
|
async def get_throttle_delay(self, run_id: str, base_delay: float = 0.0) -> float:
|
||
|
|
"""Calculate throttle delay based on factor. Returns additional sleep time."""
|
||
|
|
factor = self._throttle_factors.get(run_id, 1.0)
|
||
|
|
if factor >= 1.0:
|
||
|
|
return 0.0
|
||
|
|
# Invert: lower factor = more delay
|
||
|
|
return (1.0 / max(factor, 0.01) - 1.0) + base_delay
|
||
|
|
|
||
|
|
async def get_run_status(self, run_id: str) -> Optional[Dict[str, Any]]:
|
||
|
|
"""Get status of a specific run."""
|
||
|
|
return self._runs.get(run_id)
|
||
|
|
|
||
|
|
async def list_runs(self) -> List[Dict[str, Any]]:
|
||
|
|
"""List all registered runs."""
|
||
|
|
return [
|
||
|
|
{"run_id": rid, **info}
|
||
|
|
for rid, info in self._runs.items()
|
||
|
|
]
|
||
|
|
|
||
|
|
async def update_progress(self, run_id: str, glyph_count: Optional[int] = None,
|
||
|
|
steps: Optional[int] = None) -> None:
|
||
|
|
"""Update progress metrics for a run."""
|
||
|
|
async with self._lock:
|
||
|
|
run = self._runs.get(run_id)
|
||
|
|
if run is None:
|
||
|
|
return
|
||
|
|
if glyph_count is not None:
|
||
|
|
run["glyph_count"] = glyph_count
|
||
|
|
if steps is not None:
|
||
|
|
run["steps_executed"] = steps
|
||
|
|
|
||
|
|
pipeline_control = PipelineControl()
|
||
|
|
|
||
|
|
# ========================
|
||
|
|
# API Endpoints
|
||
|
|
# ========================
|
||
|
|
|
||
|
|
@app.get("/api/vram/mode")
|
||
|
|
async def get_vram_mode():
|
||
|
|
"""Get current VRAM mode configuration"""
|
||
|
|
global CURRENT_VRAM_MODE
|
||
|
|
config = VRAM_CONFIGS.get(CURRENT_VRAM_MODE, VRAM_CONFIGS["8GB"])
|
||
|
|
return {
|
||
|
|
"status": "success",
|
||
|
|
"current_mode": CURRENT_VRAM_MODE,
|
||
|
|
"config": config
|
||
|
|
}
|
||
|
|
|
||
|
|
class VRAMModeRequest(BaseModel):
|
||
|
|
mode: str = Field(..., pattern="^(8GB|24GB|48GB)$")
|
||
|
|
|
||
|
|
@app.post("/api/set-vram-mode")
|
||
|
|
async def set_vram_mode(req: VRAMModeRequest):
|
||
|
|
"""Switch VRAM mode for compressed-model strategy
|
||
|
|
|
||
|
|
Switches between:
|
||
|
|
- 8GB: CPU offload mode (safe for GTX 1080)
|
||
|
|
- 24GB: Full GPU + unified memory
|
||
|
|
- 48GB: Multi-GPU + max capacity
|
||
|
|
"""
|
||
|
|
global CURRENT_VRAM_MODE
|
||
|
|
mode = req.mode
|
||
|
|
|
||
|
|
if mode not in VRAM_CONFIGS:
|
||
|
|
raise HTTPException(status_code=400, detail=f"Invalid VRAM mode. Must be one of: {list(VRAM_CONFIGS.keys())}")
|
||
|
|
|
||
|
|
CURRENT_VRAM_MODE = mode
|
||
|
|
config = VRAM_CONFIGS[mode]
|
||
|
|
|
||
|
|
logger.info(f"VRAM mode switched to {mode}: {config['description']}")
|
||
|
|
|
||
|
|
return {"status": "ok", "mode": mode, "config": config}
|
||
|
|
|
||
|
|
@app.get("/api/status")
|
||
|
|
async def get_status(authorization: Optional[str] = Header(None)):
|
||
|
|
"""System health and VRAM status"""
|
||
|
|
user_id = authorization.replace("Bearer ", "") if authorization else "anonymous"
|
||
|
|
vram = get_vram_usage()
|
||
|
|
|
||
|
|
return {
|
||
|
|
"status": "operational" if vram["used_gb"] < VRAM_CRITICAL else "warning",
|
||
|
|
"timestamp": datetime.now().isoformat(),
|
||
|
|
"vram": vram,
|
||
|
|
"vram_status": (
|
||
|
|
"VRAM safe" if vram["used_gb"] < VRAM_WARNING else
|
||
|
|
"⚠️ High VRAM" if vram["used_gb"] < VRAM_CRITICAL else
|
||
|
|
"🚨 CRITICAL - stop models"
|
||
|
|
),
|
||
|
|
"models_running": {
|
||
|
|
"llama": "checking...",
|
||
|
|
"forge": "checking...",
|
||
|
|
"janus": "checking...",
|
||
|
|
"google_ai": "available" if GOOGLE_API_KEY else "unconfigured"
|
||
|
|
},
|
||
|
|
"vram_mode": CURRENT_VRAM_MODE,
|
||
|
|
"compression": {
|
||
|
|
"enabled": True,
|
||
|
|
"format": "GSZ3",
|
||
|
|
"glyphmart": "ready"
|
||
|
|
},
|
||
|
|
"conflict_check": "OK"
|
||
|
|
}
|
||
|
|
|
||
|
|
@app.get("/api/config")
|
||
|
|
async def get_config():
|
||
|
|
"""System configuration"""
|
||
|
|
# Define default Pinokio endpoints if not set
|
||
|
|
PINOKIO_ENDPOINTS = {
|
||
|
|
"llama": "http://localhost:11436",
|
||
|
|
"forge": "http://localhost:8001",
|
||
|
|
"janus": "http://localhost:8002",
|
||
|
|
"google_ai": "https://generativelanguage.googleapis.com"
|
||
|
|
}
|
||
|
|
|
||
|
|
return {
|
||
|
|
"hardware": {
|
||
|
|
"gpu": "GTX 1080",
|
||
|
|
"vram": "8GB",
|
||
|
|
"platform": "Pinokio",
|
||
|
|
"pinokio_endpoints": PINOKIO_ENDPOINTS
|
||
|
|
},
|
||
|
|
"models": {
|
||
|
|
"chat": "Llama (via Pinokio)",
|
||
|
|
"image_gen": "Stable Diffusion Forge",
|
||
|
|
"vision": "Google Gemini 1.5 Pro",
|
||
|
|
"video": "Janus-Pro-7B",
|
||
|
|
"compressed": "GlyphMart (GSZ3/XIC)"
|
||
|
|
},
|
||
|
|
"api_version": "2.0.0",
|
||
|
|
"backend_status": "ready",
|
||
|
|
"features": [
|
||
|
|
"Chat with Llama",
|
||
|
|
"Image generation (Forge)",
|
||
|
|
"Video generation (Janus)",
|
||
|
|
"Vision analysis (Google AI)",
|
||
|
|
"Autonomous memory (ORACLE)",
|
||
|
|
"Web access & scraping",
|
||
|
|
"User session tracking"
|
||
|
|
]
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
@app.get("/api/image-history")
|
||
|
|
async def get_image_history():
|
||
|
|
"""Get list of recently generated images"""
|
||
|
|
try:
|
||
|
|
images = []
|
||
|
|
if OUTPUT_DIR.exists():
|
||
|
|
# Get all PNG files, sorted by modification time (newest first)
|
||
|
|
image_files = list(OUTPUT_DIR.glob("*.png"))
|
||
|
|
image_files.sort(key=lambda x: x.stat().st_mtime, reverse=True)
|
||
|
|
|
||
|
|
# Limit to last 20 images
|
||
|
|
for img_file in image_files[:20]:
|
||
|
|
stat = img_file.stat()
|
||
|
|
images.append({
|
||
|
|
"filename": img_file.name,
|
||
|
|
"url": f"/outputs/{img_file.name}",
|
||
|
|
"size": stat.st_size,
|
||
|
|
"modified": datetime.fromtimestamp(stat.st_mtime).isoformat(),
|
||
|
|
"relative_time": _format_relative_time(stat.st_mtime)
|
||
|
|
})
|
||
|
|
|
||
|
|
return {"status": "success", "images": images}
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"Error getting image history: {e}")
|
||
|
|
return {"status": "error", "message": str(e)}
|
||
|
|
|
||
|
|
|
||
|
|
def _format_relative_time(timestamp):
|
||
|
|
"""Format timestamp as relative time (e.g., '2 minutes ago')"""
|
||
|
|
now = datetime.now().timestamp()
|
||
|
|
diff = now - timestamp
|
||
|
|
|
||
|
|
if diff < 60:
|
||
|
|
return f"{int(diff)} seconds ago"
|
||
|
|
elif diff < 3600:
|
||
|
|
return f"{int(diff/60)} minutes ago"
|
||
|
|
elif diff < 86400:
|
||
|
|
return f"{int(diff/3600)} hours ago"
|
||
|
|
else:
|
||
|
|
return f"{int(diff/86400)} days ago"
|
||
|
|
|
||
|
|
|
||
|
|
@app.post("/api/chat")
|
||
|
|
async def chat(
|
||
|
|
request: Dict[str, Any],
|
||
|
|
authorization: Optional[str] = Header(None)
|
||
|
|
):
|
||
|
|
"""Chat with Llama via Pinokio (OpenAI-compatible)
|
||
|
|
|
||
|
|
Request format (OpenAI-compatible):
|
||
|
|
{
|
||
|
|
"model": "llama-3.5-35b",
|
||
|
|
"messages": [
|
||
|
|
{"role": "system", "content": "You are helpful..."},
|
||
|
|
{"role": "user", "content": "Hello"}
|
||
|
|
],
|
||
|
|
"temperature": 0.7,
|
||
|
|
"top_p": 0.9,
|
||
|
|
"max_tokens": 2000,
|
||
|
|
"glyph_activation": { # Optional: activate glyph for enhanced response
|
||
|
|
"intent": "I need creative help",
|
||
|
|
"request_type": "chat"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
Returns OpenAI-compatible response with choices, usage, etc.
|
||
|
|
"""
|
||
|
|
user_id = authorization.replace("Bearer ", "") if authorization else "anonymous"
|
||
|
|
|
||
|
|
messages = request.get("messages", [])
|
||
|
|
if not messages:
|
||
|
|
raise HTTPException(status_code=400, detail="messages array required (OpenAI format)")
|
||
|
|
|
||
|
|
model = request.get("model", "llama-3.5-35b")
|
||
|
|
temperature = request.get("temperature", 0.7)
|
||
|
|
top_p = request.get("top_p", 0.9)
|
||
|
|
|
||
|
|
logger.info(f"Chat request from {user_id}: model={model}, messages={len(messages)}")
|
||
|
|
|
||
|
|
# Optional: Activate glyph for enhanced response
|
||
|
|
glyph_context = None
|
||
|
|
if request.get("glyph_activation"):
|
||
|
|
try:
|
||
|
|
from superdave.dual_layer.symbolic_engine import get_symbolic_engine
|
||
|
|
|
||
|
|
engine = get_symbolic_engine()
|
||
|
|
glyph_intent = request["glyph_activation"].get("intent", "")
|
||
|
|
glyph_type = request["glyph_activation"].get("request_type", "chat")
|
||
|
|
|
||
|
|
glyph_result = await engine.activate_from_intent(
|
||
|
|
user_intent=glyph_intent,
|
||
|
|
request_type=glyph_type
|
||
|
|
)
|
||
|
|
|
||
|
|
if glyph_result:
|
||
|
|
glyph_context = glyph_result
|
||
|
|
logger.info(
|
||
|
|
f"Glyph activated for chat: {glyph_result.glyph_id} "
|
||
|
|
f"({glyph_result.specialized_type}), boost={glyph_result.power_boost:.2f}x"
|
||
|
|
)
|
||
|
|
except Exception as e:
|
||
|
|
logger.warning(f"Glyph activation failed: {e}")
|
||
|
|
|
||
|
|
# Execute chat with optional glyph enhancement
|
||
|
|
if glyph_context:
|
||
|
|
from superdave.glyph_model_integration import (
|
||
|
|
GlyphExecutionContext, execute_with_glyph, prepare_chat_with_glyph
|
||
|
|
)
|
||
|
|
|
||
|
|
glyph_exec_context = GlyphExecutionContext(
|
||
|
|
glyph_id=glyph_context.glyph_id,
|
||
|
|
specialized_type=glyph_context.specialized_type,
|
||
|
|
power_boost=glyph_context.power_boost,
|
||
|
|
resonance_score=glyph_context.resonance_score,
|
||
|
|
superpower_ids=glyph_context.superpower_ids,
|
||
|
|
model=glyph_context.model,
|
||
|
|
priority=glyph_context.priority,
|
||
|
|
constraints=glyph_context.constraints,
|
||
|
|
enhancements=glyph_context.enhancements,
|
||
|
|
)
|
||
|
|
|
||
|
|
chat_params = prepare_chat_with_glyph(glyph_exec_context, messages)
|
||
|
|
chat_params.pop("glyph_context", None) # avoid conflict with positional arg
|
||
|
|
|
||
|
|
result = await execute_with_glyph(
|
||
|
|
glyph_exec_context,
|
||
|
|
lambda **kwargs: LlamaConnector.chat(
|
||
|
|
kwargs["messages"],
|
||
|
|
model,
|
||
|
|
kwargs.get("temperature", temperature),
|
||
|
|
top_p,
|
||
|
|
user_id
|
||
|
|
),
|
||
|
|
**chat_params
|
||
|
|
)
|
||
|
|
else:
|
||
|
|
result = await LlamaConnector.chat(messages, model, temperature, top_p, user_id)
|
||
|
|
|
||
|
|
# Check for Pinokio connection errors
|
||
|
|
if result.get("status") == "error":
|
||
|
|
logger.error(f"Pinokio error: {result.get('message')}")
|
||
|
|
raise HTTPException(status_code=503, detail=result.get("message", "Pinokio unavailable"))
|
||
|
|
|
||
|
|
return result
|
||
|
|
|
||
|
|
@app.post("/api/generate-image")
|
||
|
|
async def generate_image(
|
||
|
|
request: Dict[str, Any],
|
||
|
|
authorization: Optional[str] = Header(None)
|
||
|
|
):
|
||
|
|
"""Generate image with Stable Diffusion Forge
|
||
|
|
|
||
|
|
Request format:
|
||
|
|
{
|
||
|
|
"prompt": "a cat sitting on a chair",
|
||
|
|
"negative_prompt": "blurry, ugly",
|
||
|
|
"width": 768,
|
||
|
|
"height": 768,
|
||
|
|
"steps": 30,
|
||
|
|
"guidance_scale": 7.5,
|
||
|
|
"glyph_activation": { # Optional: activate glyph for enhanced generation
|
||
|
|
"intent": "I need a creative landscape",
|
||
|
|
"request_type": "image"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
"""
|
||
|
|
user_id = authorization.replace("Bearer ", "") if authorization else "anonymous"
|
||
|
|
|
||
|
|
prompt = request.get("prompt", "")
|
||
|
|
if not prompt:
|
||
|
|
raise HTTPException(status_code=400, detail="Prompt required")
|
||
|
|
|
||
|
|
width = request.get("width", 768)
|
||
|
|
height = request.get("height", 768)
|
||
|
|
steps = request.get("steps", 30)
|
||
|
|
negative_prompt = request.get("negative_prompt", "")
|
||
|
|
guidance_scale = request.get("guidance_scale", 7.5)
|
||
|
|
|
||
|
|
# Optional: Activate glyph for enhanced generation
|
||
|
|
glyph_context = None
|
||
|
|
if request.get("glyph_activation"):
|
||
|
|
try:
|
||
|
|
from superdave.dual_layer.symbolic_engine import get_symbolic_engine
|
||
|
|
|
||
|
|
engine = get_symbolic_engine()
|
||
|
|
glyph_intent = request["glyph_activation"].get("intent", "")
|
||
|
|
glyph_type = request["glyph_activation"].get("request_type", "image")
|
||
|
|
|
||
|
|
glyph_result = await engine.activate_from_intent(
|
||
|
|
user_intent=glyph_intent,
|
||
|
|
request_type=glyph_type
|
||
|
|
)
|
||
|
|
|
||
|
|
if glyph_result:
|
||
|
|
glyph_context = glyph_result
|
||
|
|
logger.info(
|
||
|
|
f"Glyph activated for image: {glyph_result.glyph_id} "
|
||
|
|
f"({glyph_result.specialized_type}), boost={glyph_result.power_boost:.2f}x"
|
||
|
|
)
|
||
|
|
except Exception as e:
|
||
|
|
logger.warning(f"Glyph activation failed for image: {e}")
|
||
|
|
|
||
|
|
# Execute generation with optional glyph enhancement
|
||
|
|
if glyph_context:
|
||
|
|
from superdave.glyph_model_integration import (
|
||
|
|
GlyphExecutionContext, execute_with_glyph, prepare_image_with_glyph
|
||
|
|
)
|
||
|
|
|
||
|
|
glyph_exec_context = GlyphExecutionContext(
|
||
|
|
glyph_id=glyph_context.glyph_id,
|
||
|
|
specialized_type=glyph_context.specialized_type,
|
||
|
|
power_boost=glyph_context.power_boost,
|
||
|
|
resonance_score=glyph_context.resonance_score,
|
||
|
|
superpower_ids=glyph_context.superpower_ids,
|
||
|
|
model=glyph_context.model,
|
||
|
|
priority=glyph_context.priority,
|
||
|
|
constraints=glyph_context.constraints,
|
||
|
|
enhancements=glyph_context.enhancements,
|
||
|
|
)
|
||
|
|
|
||
|
|
image_params = prepare_image_with_glyph(glyph_exec_context, prompt)
|
||
|
|
# User-requested values override glyph defaults
|
||
|
|
image_params["negative_prompt"] = negative_prompt
|
||
|
|
image_params["width"] = width
|
||
|
|
image_params["height"] = height
|
||
|
|
image_params["steps"] = steps
|
||
|
|
image_params["guidance_scale"] = guidance_scale
|
||
|
|
# Remove glyph_context to avoid conflict with positional arg
|
||
|
|
image_params.pop("glyph_context", None)
|
||
|
|
|
||
|
|
result = await execute_with_glyph(
|
||
|
|
glyph_exec_context,
|
||
|
|
lambda **kwargs: ForgeConnector.generate(
|
||
|
|
kwargs["prompt"],
|
||
|
|
kwargs.get("width", width),
|
||
|
|
kwargs.get("height", height),
|
||
|
|
kwargs.get("steps", steps),
|
||
|
|
kwargs.get("negative_prompt", negative_prompt),
|
||
|
|
kwargs.get("guidance_scale", guidance_scale),
|
||
|
|
user_id
|
||
|
|
),
|
||
|
|
**image_params
|
||
|
|
)
|
||
|
|
else:
|
||
|
|
result = await ForgeConnector.generate(
|
||
|
|
prompt, width, height, steps, negative_prompt, guidance_scale, user_id
|
||
|
|
)
|
||
|
|
return result
|
||
|
|
|
||
|
|
@app.post("/api/generate-video")
|
||
|
|
async def generate_video(
|
||
|
|
request: Dict[str, Any],
|
||
|
|
authorization: Optional[str] = Header(None)
|
||
|
|
):
|
||
|
|
"""Generate video with Janus-Pro-7B"""
|
||
|
|
user_id = authorization.replace("Bearer ", "") if authorization else "anonymous"
|
||
|
|
|
||
|
|
prompt = request.get("prompt", "")
|
||
|
|
if not prompt:
|
||
|
|
raise HTTPException(status_code=400, detail="Prompt required")
|
||
|
|
|
||
|
|
duration = request.get("duration", 5.0)
|
||
|
|
fps = request.get("fps", 30)
|
||
|
|
width = request.get("width", 512)
|
||
|
|
height = request.get("height", 512)
|
||
|
|
|
||
|
|
result = await JanusConnector.generate(prompt, duration, fps, width, height, user_id)
|
||
|
|
return result
|
||
|
|
|
||
|
|
@app.post("/api/vision")
|
||
|
|
async def analyze_vision(
|
||
|
|
request: Dict[str, Any],
|
||
|
|
authorization: Optional[str] = Header(None)
|
||
|
|
):
|
||
|
|
"""Analyze image with Google Gemini"""
|
||
|
|
user_id = authorization.replace("Bearer ", "") if authorization else "anonymous"
|
||
|
|
|
||
|
|
image_path = request.get("image_path", "")
|
||
|
|
prompt = request.get("prompt", "Analyze this image in detail")
|
||
|
|
|
||
|
|
if not image_path:
|
||
|
|
raise HTTPException(status_code=400, detail="image_path required")
|
||
|
|
|
||
|
|
if not Path(image_path).exists():
|
||
|
|
raise HTTPException(status_code=400, detail="Image file not found")
|
||
|
|
|
||
|
|
result = await GoogleAIConnector.analyze(image_path, prompt, user_id)
|
||
|
|
return result
|
||
|
|
|
||
|
|
@app.post("/api/web-fetch")
|
||
|
|
async def web_fetch(
|
||
|
|
request: Dict[str, Any],
|
||
|
|
authorization: Optional[str] = Header(None)
|
||
|
|
):
|
||
|
|
"""Fetch and parse web content"""
|
||
|
|
user_id = authorization.replace("Bearer ", "") if authorization else "anonymous"
|
||
|
|
|
||
|
|
url = request.get("url", "")
|
||
|
|
if not url:
|
||
|
|
raise HTTPException(status_code=400, detail="URL required")
|
||
|
|
|
||
|
|
result = await fetch_url(url)
|
||
|
|
oracle.session_log(user_id, "web_fetch", {"url": url})
|
||
|
|
return result
|
||
|
|
|
||
|
|
@app.post("/api/oracle/remember")
|
||
|
|
async def oracle_remember(
|
||
|
|
request: Dict[str, Any],
|
||
|
|
authorization: Optional[str] = Header(None)
|
||
|
|
):
|
||
|
|
"""Store memory in ORACLE"""
|
||
|
|
user_id = authorization.replace("Bearer ", "") if authorization else "anonymous"
|
||
|
|
|
||
|
|
key = request.get("key", "")
|
||
|
|
value = request.get("value", "")
|
||
|
|
category = request.get("category", "facts")
|
||
|
|
|
||
|
|
if not key:
|
||
|
|
raise HTTPException(status_code=400, detail="Key required")
|
||
|
|
|
||
|
|
oracle.remember(key, value, category)
|
||
|
|
oracle.session_log(user_id, "memory_store", {"key": key})
|
||
|
|
|
||
|
|
return {"status": "stored", "key": key, "value": value}
|
||
|
|
|
||
|
|
@app.post("/api/oracle/recall")
|
||
|
|
async def oracle_recall(
|
||
|
|
request: Dict[str, Any],
|
||
|
|
authorization: Optional[str] = Header(None)
|
||
|
|
):
|
||
|
|
"""Retrieve memory from ORACLE"""
|
||
|
|
user_id = authorization.replace("Bearer ", "") if authorization else "anonymous"
|
||
|
|
|
||
|
|
key = request.get("key", "")
|
||
|
|
category = request.get("category", "facts")
|
||
|
|
|
||
|
|
if not key:
|
||
|
|
raise HTTPException(status_code=400, detail="Key required")
|
||
|
|
|
||
|
|
value = oracle.recall(key, category)
|
||
|
|
oracle.session_log(user_id, "memory_recall", {"key": key})
|
||
|
|
|
||
|
|
return {
|
||
|
|
"status": "found" if value is not None else "not_found",
|
||
|
|
"key": key,
|
||
|
|
"value": value
|
||
|
|
}
|
||
|
|
|
||
|
|
@app.post("/api/oracle/forget")
|
||
|
|
async def oracle_forget(
|
||
|
|
request: Dict[str, Any],
|
||
|
|
authorization: Optional[str] = Header(None)
|
||
|
|
):
|
||
|
|
"""Delete memory from ORACLE"""
|
||
|
|
user_id = authorization.replace("Bearer ", "") if authorization else "anonymous"
|
||
|
|
|
||
|
|
key = request.get("key", "")
|
||
|
|
category = request.get("category", "facts")
|
||
|
|
|
||
|
|
if not key:
|
||
|
|
raise HTTPException(status_code=400, detail="Key required")
|
||
|
|
|
||
|
|
oracle.forget(key, category)
|
||
|
|
oracle.session_log(user_id, "memory_delete", {"key": key})
|
||
|
|
|
||
|
|
return {"status": "forgotten", "key": key}
|
||
|
|
|
||
|
|
@app.get("/api/oracle/dump")
|
||
|
|
async def oracle_dump(authorization: Optional[str] = Header(None)):
|
||
|
|
"""Retrieve all memory (admin only)"""
|
||
|
|
user_id = authorization.replace("Bearer ", "") if authorization else "anonymous"
|
||
|
|
return oracle.memory
|
||
|
|
|
||
|
|
@app.get("/api/health")
|
||
|
|
async def health():
|
||
|
|
"""Simple health check"""
|
||
|
|
return {"status": "ok", "service": "SuperDave AI 2.0"}
|
||
|
|
|
||
|
|
@app.get("/")
|
||
|
|
async def root():
|
||
|
|
"""Root endpoint"""
|
||
|
|
return {
|
||
|
|
"service": "SuperDave AI 2.0",
|
||
|
|
"version": "2.0.0",
|
||
|
|
"status": "running",
|
||
|
|
"docs": "http://localhost:8000/docs",
|
||
|
|
"features": [
|
||
|
|
"Chat with Llama",
|
||
|
|
"Image generation (Forge/SD)",
|
||
|
|
"Video generation (Janus)",
|
||
|
|
"Vision analysis (Google AI)",
|
||
|
|
"Autonomous memory (ORACLE)",
|
||
|
|
"Web access & scraping",
|
||
|
|
"Compressed execution (GlyphMart/GSZ3)"
|
||
|
|
]
|
||
|
|
}
|
||
|
|
|
||
|
|
# ========================
|
||
|
|
# Startup/Shutdown
|
||
|
|
# ========================
|
||
|
|
|
||
|
|
@app.websocket("/ws/fedmart/xic")
|
||
|
|
async def websocket_fedmart(websocket: WebSocket):
|
||
|
|
"""WebSocket endpoint for real-time XIC telemetry streaming"""
|
||
|
|
await broadcast_manager.connect(websocket)
|
||
|
|
try:
|
||
|
|
while True:
|
||
|
|
data = await websocket.receive_text()
|
||
|
|
# Echo back for client-side acks, or process control messages
|
||
|
|
logger.debug(f"[FEDMART] WebSocket message: {data}")
|
||
|
|
except WebSocketDisconnect:
|
||
|
|
broadcast_manager.disconnect(websocket)
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"[FEDMART] WebSocket error: {e}")
|
||
|
|
broadcast_manager.disconnect(websocket)
|
||
|
|
|
||
|
|
@app.post("/fedmart/ingest/xic")
|
||
|
|
async def ingest_xic_telemetry(
|
||
|
|
request: Dict[str, Any],
|
||
|
|
authorization: Optional[str] = Header(None)
|
||
|
|
):
|
||
|
|
"""Ingest XIC telemetry events from XIC pipeline.
|
||
|
|
|
||
|
|
Accepts telemetry dict with:
|
||
|
|
- event_type: str
|
||
|
|
- timestamp: ISO 8601
|
||
|
|
- run_id: str
|
||
|
|
- glyph_ids: List[str]
|
||
|
|
- glyph_count: int
|
||
|
|
- global_resonance_score: float
|
||
|
|
- steps_executed: int
|
||
|
|
- guardrails_triggered: List[str]
|
||
|
|
- resonance_map_summary: dict (optional)
|
||
|
|
- raw_payload: dict (optional)
|
||
|
|
"""
|
||
|
|
user_id = authorization.replace("Bearer ", "") if authorization else "anonymous"
|
||
|
|
|
||
|
|
try:
|
||
|
|
# Validate required fields
|
||
|
|
required = ["event_type", "glyph_count", "global_resonance_score", "steps_executed"]
|
||
|
|
for field in required:
|
||
|
|
if field not in request:
|
||
|
|
raise HTTPException(status_code=400, detail=f"Missing required field: {field}")
|
||
|
|
|
||
|
|
# Buffer locally
|
||
|
|
telemetry_buffer.append(request)
|
||
|
|
if len(telemetry_buffer) > max_buffer_size:
|
||
|
|
telemetry_buffer.pop(0)
|
||
|
|
|
||
|
|
# Broadcast to WebSocket clients
|
||
|
|
await broadcast_manager.broadcast(request)
|
||
|
|
|
||
|
|
logger.info(f"[FEDMART] Telemetry ingested from {user_id}: run_id={request.get('run_id')}, "
|
||
|
|
f"glyphs={request.get('glyph_count')}, score={request.get('global_resonance_score'):.3f}")
|
||
|
|
|
||
|
|
return {
|
||
|
|
"status": "accepted",
|
||
|
|
"run_id": request.get("run_id"),
|
||
|
|
"buffer_size": len(telemetry_buffer)
|
||
|
|
}
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"[FEDMART] Ingest error: {e}")
|
||
|
|
raise HTTPException(status_code=500, detail=str(e))
|
||
|
|
|
||
|
|
@app.get("/fedmart/telemetry/recent")
|
||
|
|
async def get_recent_telemetry(
|
||
|
|
limit: int = 10,
|
||
|
|
authorization: Optional[str] = Header(None)
|
||
|
|
):
|
||
|
|
"""Retrieve recent telemetry events from buffer"""
|
||
|
|
user_id = authorization.replace("Bearer ", "") if authorization else "anonymous"
|
||
|
|
|
||
|
|
recent = telemetry_buffer[-limit:] if telemetry_buffer else []
|
||
|
|
logger.info(f"[FEDMART] Telemetry retrieved by {user_id}: {len(recent)} events")
|
||
|
|
|
||
|
|
return {
|
||
|
|
"status": "success",
|
||
|
|
"count": len(recent),
|
||
|
|
"telemetry": recent
|
||
|
|
}
|
||
|
|
|
||
|
|
@app.post("/fedmart/control/pause")
|
||
|
|
async def fedmart_pause_run(
|
||
|
|
request: Dict[str, Any],
|
||
|
|
authorization: Optional[str] = Header(None)
|
||
|
|
):
|
||
|
|
"""Pause a running XIC pipeline (guardrail control action)"""
|
||
|
|
user_id = authorization.replace("Bearer ", "") if authorization else "anonymous"
|
||
|
|
|
||
|
|
run_id = request.get("run_id", "unknown")
|
||
|
|
logger.info(f"[FEDMART-CONTROL] Pause requested for run {run_id} by {user_id}")
|
||
|
|
|
||
|
|
if run_id == "unknown" or run_id not in pipeline_control._runs:
|
||
|
|
# Auto-register if not tracked (allow control of externally-managed runs)
|
||
|
|
await pipeline_control.register_run(run_id)
|
||
|
|
logger.info(f"[FEDMART-CONTROL] Auto-registered run {run_id} for control")
|
||
|
|
|
||
|
|
success = await pipeline_control.pause(run_id)
|
||
|
|
status = pipeline_control._runs.get(run_id, {})
|
||
|
|
|
||
|
|
return {
|
||
|
|
"status": "accepted" if success else "error",
|
||
|
|
"action": "pause",
|
||
|
|
"run_id": run_id,
|
||
|
|
"pipeline_status": status.get("status", "unknown") if success else "not_found",
|
||
|
|
"message": f"Pause signal sent to run {run_id}" if success else f"Run {run_id} not found"
|
||
|
|
}
|
||
|
|
|
||
|
|
@app.post("/fedmart/control/resume")
|
||
|
|
async def fedmart_resume_run(
|
||
|
|
request: Dict[str, Any],
|
||
|
|
authorization: Optional[str] = Header(None)
|
||
|
|
):
|
||
|
|
"""Resume a paused XIC pipeline"""
|
||
|
|
user_id = authorization.replace("Bearer ", "") if authorization else "anonymous"
|
||
|
|
|
||
|
|
run_id = request.get("run_id", "unknown")
|
||
|
|
logger.info(f"[FEDMART-CONTROL] Resume requested for run {run_id} by {user_id}")
|
||
|
|
|
||
|
|
success = await pipeline_control.resume(run_id)
|
||
|
|
|
||
|
|
return {
|
||
|
|
"status": "accepted" if success else "error",
|
||
|
|
"action": "resume",
|
||
|
|
"run_id": run_id,
|
||
|
|
"message": f"Resume signal sent to run {run_id}" if success else f"Run {run_id} not found"
|
||
|
|
}
|
||
|
|
|
||
|
|
@app.post("/fedmart/control/throttle")
|
||
|
|
async def fedmart_throttle_run(
|
||
|
|
request: Dict[str, Any],
|
||
|
|
authorization: Optional[str] = Header(None)
|
||
|
|
):
|
||
|
|
"""Throttle a running XIC pipeline (reduce execution speed)"""
|
||
|
|
user_id = authorization.replace("Bearer ", "") if authorization else "anonymous"
|
||
|
|
|
||
|
|
run_id = request.get("run_id", "unknown")
|
||
|
|
factor = request.get("factor", 0.5)
|
||
|
|
|
||
|
|
logger.info(f"[FEDMART-CONTROL] Throttle {factor:.1%} requested for run {run_id} by {user_id}")
|
||
|
|
|
||
|
|
if run_id == "unknown" or run_id not in pipeline_control._runs:
|
||
|
|
await pipeline_control.register_run(run_id)
|
||
|
|
|
||
|
|
success = await pipeline_control.throttle(run_id, factor)
|
||
|
|
status = pipeline_control._runs.get(run_id, {})
|
||
|
|
|
||
|
|
return {
|
||
|
|
"status": "accepted" if success else "error",
|
||
|
|
"action": "throttle",
|
||
|
|
"run_id": run_id,
|
||
|
|
"factor": factor,
|
||
|
|
"pipeline_status": status.get("status", "unknown") if success else "not_found",
|
||
|
|
"message": f"Throttle signal sent to run {run_id} at {factor:.1%}" if success else f"Run {run_id} not found"
|
||
|
|
}
|
||
|
|
|
||
|
|
@app.get("/fedmart/control/runs")
|
||
|
|
async def fedmart_list_runs(
|
||
|
|
authorization: Optional[str] = Header(None)
|
||
|
|
):
|
||
|
|
"""List all registered pipeline runs and their states"""
|
||
|
|
user_id = authorization.replace("Bearer ", "") if authorization else "anonymous"
|
||
|
|
runs = await pipeline_control.list_runs()
|
||
|
|
return {
|
||
|
|
"status": "success",
|
||
|
|
"count": len(runs),
|
||
|
|
"runs": runs
|
||
|
|
}
|
||
|
|
|
||
|
|
@app.get("/fedmart/control/runs/{run_id}")
|
||
|
|
async def fedmart_get_run(
|
||
|
|
run_id: str,
|
||
|
|
authorization: Optional[str] = Header(None)
|
||
|
|
):
|
||
|
|
"""Get status of a specific pipeline run"""
|
||
|
|
user_id = authorization.replace("Bearer ", "") if authorization else "anonymous"
|
||
|
|
run = await pipeline_control.get_run_status(run_id)
|
||
|
|
if run is None:
|
||
|
|
raise HTTPException(status_code=404, detail=f"Run {run_id} not found")
|
||
|
|
return {
|
||
|
|
"status": "success",
|
||
|
|
"run_id": run_id,
|
||
|
|
**run
|
||
|
|
}
|
||
|
|
|
||
|
|
@app.post("/fedmart/spec_map")
|
||
|
|
async def register_spec_map(
|
||
|
|
request: Dict[str, Any],
|
||
|
|
authorization: Optional[str] = Header(None)
|
||
|
|
):
|
||
|
|
"""Register XIC specification status map"""
|
||
|
|
user_id = authorization.replace("Bearer ", "") if authorization else "anonymous"
|
||
|
|
|
||
|
|
spec_map = request.get("spec_map", {})
|
||
|
|
if not spec_map:
|
||
|
|
raise HTTPException(status_code=400, detail="spec_map required")
|
||
|
|
|
||
|
|
logger.info(f"[FEDMART] Spec map registered by {user_id}: {len(spec_map)} entries")
|
||
|
|
|
||
|
|
return {
|
||
|
|
"status": "registered",
|
||
|
|
"count": len(spec_map),
|
||
|
|
"entries": list(spec_map.keys())
|
||
|
|
}
|
||
|
|
|
||
|
|
@app.get("/fedmart/status")
|
||
|
|
async def fedmart_status(authorization: Optional[str] = Header(None)):
|
||
|
|
"""FedMart system status"""
|
||
|
|
user_id = authorization.replace("Bearer ", "") if authorization else "anonymous"
|
||
|
|
|
||
|
|
return {
|
||
|
|
"status": "operational",
|
||
|
|
"service": "FedMart Telemetry Integration",
|
||
|
|
"timestamp": datetime.now().isoformat(),
|
||
|
|
"connections": len(broadcast_manager.active_connections),
|
||
|
|
"telemetry_buffer": {
|
||
|
|
"size": len(telemetry_buffer),
|
||
|
|
"max_size": max_buffer_size
|
||
|
|
},
|
||
|
|
"features": [
|
||
|
|
"XIC telemetry ingestion",
|
||
|
|
"Real-time WebSocket broadcast",
|
||
|
|
"Guardrail control actions (pause, throttle)",
|
||
|
|
"Specification status tracking"
|
||
|
|
]
|
||
|
|
}
|
||
|
|
|
||
|
|
@app.get("/", include_in_schema=False)
|
||
|
|
async def root():
|
||
|
|
return {
|
||
|
|
"status": "ok",
|
||
|
|
"service": "SuperDave AI 2.0",
|
||
|
|
"version": "2.0.0",
|
||
|
|
"compression": {
|
||
|
|
"enabled": True,
|
||
|
|
"format": "GSZ3",
|
||
|
|
"gx_header": "XIC v1"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
# ========================
|
||
|
|
# Compressed Execution (GlyphMart)
|
||
|
|
# ========================
|
||
|
|
|
||
|
|
class ExecuteCompressedRequest(BaseModel):
|
||
|
|
gx_path: str = Field(..., description="Path to compressed GX file")
|
||
|
|
trace: bool = Field(default=False, description="Enable execution tracing")
|
||
|
|
profile: bool = Field(default=False, description="Enable segment profiling")
|
||
|
|
user_id: str = Field(default="anonymous", description="User identifier for tracking")
|
||
|
|
|
||
|
|
@app.post("/api/execute-compressed")
|
||
|
|
async def execute_compressed(
|
||
|
|
request: ExecuteCompressedRequest,
|
||
|
|
authorization: Optional[str] = Header(None)
|
||
|
|
):
|
||
|
|
"""Execute a compressed GX file through GlyphMart (GSZ3 compressed XIC pipeline)
|
||
|
|
|
||
|
|
Executes compressed .gx binary files through the XIC symbolic pipeline with:
|
||
|
|
- GSZ3 decompression
|
||
|
|
- Segment runtime execution
|
||
|
|
- Execution tracing and profiling (optional)
|
||
|
|
- FedMart telemetry integration
|
||
|
|
"""
|
||
|
|
user_id = authorization.replace("Bearer ", "") if authorization else request.user_id
|
||
|
|
|
||
|
|
gx_path = request.gx_path
|
||
|
|
if not gx_path:
|
||
|
|
raise HTTPException(status_code=400, detail="gx_path required")
|
||
|
|
|
||
|
|
gx_file = Path(gx_path)
|
||
|
|
if not gx_file.exists():
|
||
|
|
raise HTTPException(status_code=400, detail=f"GX file not found: {gx_path}")
|
||
|
|
|
||
|
|
try:
|
||
|
|
from xic_extensions.compressed_engine import CompressedEngine, CompressionManifest
|
||
|
|
from xic_extensions.gsz3_decompressor import GSZ3Decompressor
|
||
|
|
|
||
|
|
with open(gx_path, 'rb') as f:
|
||
|
|
gx_data = f.read()
|
||
|
|
|
||
|
|
from gx_compiler.gx_packer import GXPacker
|
||
|
|
manifest, compressed_payload = GXPacker.unpack(gx_data)
|
||
|
|
|
||
|
|
engine = CompressedEngine(verbose=True, profile=request.profile)
|
||
|
|
result = engine.execute(compressed_payload, CompressionManifest(
|
||
|
|
source_file=manifest.get("source_file", gx_path),
|
||
|
|
codex_lineage=manifest.get("codex_lineage", {})
|
||
|
|
), debug=False)
|
||
|
|
|
||
|
|
fedmart_telemetry = {
|
||
|
|
"event_type": "compressed_execution",
|
||
|
|
"timestamp": datetime.now().isoformat(),
|
||
|
|
"run_id": f"glyphmart_{int(datetime.now().timestamp() * 1000)}",
|
||
|
|
"gx_path": gx_path,
|
||
|
|
"segment_count": result.get("segment_count", 0),
|
||
|
|
"traces_count": len(result.get("traces", [])),
|
||
|
|
"compressed_size": len(compressed_payload),
|
||
|
|
"status": result.get("status", "unknown")
|
||
|
|
}
|
||
|
|
|
||
|
|
try:
|
||
|
|
from integrations.fedmart.xic_adapter import emit_telemetry
|
||
|
|
emit_telemetry(fedmart_telemetry)
|
||
|
|
except Exception as e:
|
||
|
|
logger.warning(f"FedMart telemetry failed: {e}")
|
||
|
|
|
||
|
|
try:
|
||
|
|
from integrations.fedmart.glyph_telemetry import emit_compressed_execution
|
||
|
|
emit_compressed_execution(
|
||
|
|
gx_path=gx_path,
|
||
|
|
segment_count=result.get("segment_count", 0),
|
||
|
|
traces_count=len(result.get("traces", [])),
|
||
|
|
compressed_size=len(compressed_payload),
|
||
|
|
status=result.get("status", "unknown")
|
||
|
|
)
|
||
|
|
except Exception as e:
|
||
|
|
logger.warning(f"FedMart glyph telemetry failed: {e}")
|
||
|
|
|
||
|
|
return {
|
||
|
|
"status": "success",
|
||
|
|
"gx_path": gx_path,
|
||
|
|
**result
|
||
|
|
}
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"Compressed execution error: {e}")
|
||
|
|
return {
|
||
|
|
"status": "error",
|
||
|
|
"message": str(e)
|
||
|
|
}
|
||
|
|
|
||
|
|
@app.get("/api/compression/status")
|
||
|
|
async def get_compression_status():
|
||
|
|
"""Get compression system status"""
|
||
|
|
try:
|
||
|
|
from xic_extensions.gsz3_decompressor import GSZ3Decompressor
|
||
|
|
|
||
|
|
test_text = "Hello from GlyphMart compression engine!"
|
||
|
|
compressed = GSZ3Decompressor.compress(test_text)
|
||
|
|
decompressed = GSZ3Decompressor.decompress(compressed)
|
||
|
|
|
||
|
|
compression_ratio = len(test_text) / len(compressed)
|
||
|
|
|
||
|
|
return {
|
||
|
|
"status": "operational",
|
||
|
|
"compression_format": "GSZ3",
|
||
|
|
"version": 1,
|
||
|
|
"test": {
|
||
|
|
"original_size": len(test_text),
|
||
|
|
"compressed_size": len(compressed),
|
||
|
|
"decompressed_matches": test_text == decompressed,
|
||
|
|
"compression_ratio": round(compression_ratio, 2)
|
||
|
|
},
|
||
|
|
"features": [
|
||
|
|
"GSZ3 compression (zlib level 9)",
|
||
|
|
"GX binary format (XIC magic header)",
|
||
|
|
"Segment runtime execution",
|
||
|
|
"Execution tracing & profiling"
|
||
|
|
]
|
||
|
|
}
|
||
|
|
except Exception as e:
|
||
|
|
return {
|
||
|
|
"status": "error",
|
||
|
|
"message": str(e)
|
||
|
|
}
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
port = int(os.getenv("PORT", 8000))
|
||
|
|
uvicorn.run(
|
||
|
|
app,
|
||
|
|
host="0.0.0.0",
|
||
|
|
port=port,
|
||
|
|
log_level="info"
|
||
|
|
)
|