fix: update Swapper endpoints (/health, /models), remove upload size limits, auto-convert images
This commit is contained in:
@@ -158,7 +158,7 @@ export function AgentAvatarUpload({
|
|||||||
{canEdit && (
|
{canEdit && (
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<p className="text-sm text-white/50 mb-3">
|
<p className="text-sm text-white/50 mb-3">
|
||||||
Upload a custom avatar for this agent. Recommended size: 256x256px.
|
Upload a custom avatar for this agent. Any image format accepted.
|
||||||
</p>
|
</p>
|
||||||
<input
|
<input
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
|
|||||||
@@ -200,21 +200,26 @@ class NodeGuardian:
|
|||||||
# Collect Swapper Metrics
|
# Collect Swapper Metrics
|
||||||
swapper_url = os.getenv("SWAPPER_URL", "http://swapper-service:8890")
|
swapper_url = os.getenv("SWAPPER_URL", "http://swapper-service:8890")
|
||||||
try:
|
try:
|
||||||
# Check healthz
|
# Check health (Swapper uses /health, not /healthz)
|
||||||
try:
|
try:
|
||||||
r = await self.client.get(f"{swapper_url}/healthz", timeout=3.0)
|
r = await self.client.get(f"{swapper_url}/health", timeout=3.0)
|
||||||
metrics["swapper_healthy"] = (r.status_code == 200)
|
if r.status_code == 200:
|
||||||
|
health_data = r.json()
|
||||||
|
metrics["swapper_healthy"] = health_data.get("status") == "healthy"
|
||||||
|
else:
|
||||||
|
metrics["swapper_healthy"] = False
|
||||||
except Exception:
|
except Exception:
|
||||||
metrics["swapper_healthy"] = False
|
metrics["swapper_healthy"] = False
|
||||||
|
|
||||||
# Check models
|
# Check models (Swapper uses /models, not /v1/models)
|
||||||
try:
|
try:
|
||||||
r = await self.client.get(f"{swapper_url}/v1/models", timeout=5.0)
|
r = await self.client.get(f"{swapper_url}/models", timeout=5.0)
|
||||||
if r.status_code == 200:
|
if r.status_code == 200:
|
||||||
data = r.json()
|
data = r.json()
|
||||||
models = data.get("models", [])
|
models = data.get("models", [])
|
||||||
metrics["swapper_models_total"] = len(models)
|
metrics["swapper_models_total"] = len(models)
|
||||||
metrics["swapper_models_loaded"] = sum(1 for m in models if m.get("loaded") is True)
|
# Swapper uses "status": "loaded" not "loaded": true
|
||||||
|
metrics["swapper_models_loaded"] = sum(1 for m in models if m.get("status") == "loaded")
|
||||||
metrics["swapper_state"] = data
|
metrics["swapper_state"] = data
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# logger.warning(f"Failed to fetch Swapper models: {e}")
|
# logger.warning(f"Failed to fetch Swapper models: {e}")
|
||||||
|
|||||||
@@ -39,3 +39,4 @@ echo " docker-compose -f docker-compose.city-space.yml logs -f"
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -42,3 +42,4 @@ echo " ./scripts/test-phase2-e2e.sh"
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -51,3 +51,4 @@ echo ""
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -55,3 +55,4 @@ echo "📚 Documentation: docs/PHASE4_READY.md"
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -14,3 +14,4 @@ echo "✅ Services stopped!"
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -17,3 +17,4 @@ echo " docker-compose -f docker-compose.agents.yml down -v"
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -16,3 +16,4 @@ echo ""
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -11,3 +11,4 @@ echo "✅ Phase 4 services stopped"
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -204,3 +204,4 @@ fi
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -23,3 +23,4 @@ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7001"]
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -346,3 +346,4 @@ Proprietary — DAARION Ecosystem
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -69,41 +69,32 @@ AUTH_SERVICE_URL = os.getenv("AUTH_SERVICE_URL", "http://daarion-auth:7020")
|
|||||||
MATRIX_GATEWAY_URL = os.getenv("MATRIX_GATEWAY_URL", "http://daarion-matrix-gateway:7025")
|
MATRIX_GATEWAY_URL = os.getenv("MATRIX_GATEWAY_URL", "http://daarion-matrix-gateway:7025")
|
||||||
|
|
||||||
# Helper for image processing
|
# Helper for image processing
|
||||||
def process_image(image_bytes: bytes, target_size: tuple = (256, 256)) -> tuple[bytes, bytes]:
|
def process_image(image_bytes: bytes, max_size: int = 1024, force_square: bool = False) -> tuple[bytes, bytes]:
|
||||||
"""
|
"""
|
||||||
Process image:
|
Process image:
|
||||||
1. Convert to PNG
|
1. Convert to PNG (any format accepted)
|
||||||
2. Resize/Crop to target_size (default 256x256)
|
2. Resize to fit within max_size (preserving aspect ratio)
|
||||||
3. Generate thumbnail 128x128
|
3. Optionally force square crop for avatars/logos
|
||||||
|
4. Generate thumbnail 128x128
|
||||||
Returns (processed_bytes, thumb_bytes)
|
Returns (processed_bytes, thumb_bytes)
|
||||||
"""
|
"""
|
||||||
with Image.open(io.BytesIO(image_bytes)) as img:
|
with Image.open(io.BytesIO(image_bytes)) as img:
|
||||||
# Convert to RGBA/RGB
|
# Convert to RGBA/RGB
|
||||||
if img.mode in ('P', 'CMYK'):
|
if img.mode in ('P', 'CMYK', 'LA'):
|
||||||
img = img.convert('RGBA')
|
img = img.convert('RGBA')
|
||||||
|
elif img.mode != 'RGBA':
|
||||||
|
img = img.convert('RGB')
|
||||||
|
|
||||||
# Resize/Crop to target_size
|
# Force square crop if needed (for avatars/logos)
|
||||||
img_ratio = img.width / img.height
|
if force_square:
|
||||||
target_ratio = target_size[0] / target_size[1]
|
min_dim = min(img.width, img.height)
|
||||||
|
left = (img.width - min_dim) / 2
|
||||||
if img_ratio > target_ratio:
|
top = (img.height - min_dim) / 2
|
||||||
# Wider than target
|
img = img.crop((left, top, left + min_dim, top + min_dim))
|
||||||
new_height = target_size[1]
|
|
||||||
new_width = int(new_height * img_ratio)
|
|
||||||
else:
|
|
||||||
# Taller than target
|
|
||||||
new_width = target_size[0]
|
|
||||||
new_height = int(new_width / img_ratio)
|
|
||||||
|
|
||||||
img = img.resize((new_width, new_height), Image.Resampling.LANCZOS)
|
# Resize to fit within max_size (preserve aspect ratio)
|
||||||
|
if img.width > max_size or img.height > max_size:
|
||||||
# Center crop
|
img.thumbnail((max_size, max_size), Image.Resampling.LANCZOS)
|
||||||
left = (new_width - target_size[0]) / 2
|
|
||||||
top = (new_height - target_size[1]) / 2
|
|
||||||
right = (new_width + target_size[0]) / 2
|
|
||||||
bottom = (new_height + target_size[1]) / 2
|
|
||||||
|
|
||||||
img = img.crop((left, top, right, bottom))
|
|
||||||
|
|
||||||
# Save processed
|
# Save processed
|
||||||
processed_io = io.BytesIO()
|
processed_io = io.BytesIO()
|
||||||
@@ -318,25 +309,31 @@ async def update_agent_visibility_endpoint(
|
|||||||
@router.post("/assets/upload")
|
@router.post("/assets/upload")
|
||||||
async def upload_asset(
|
async def upload_asset(
|
||||||
file: UploadFile = File(...),
|
file: UploadFile = File(...),
|
||||||
type: str = Form(...) # microdao_logo, microdao_banner, room_logo, room_banner
|
type: str = Form(...) # microdao_logo, microdao_banner, room_logo, room_banner, agent_avatar
|
||||||
):
|
):
|
||||||
"""Upload asset (logo/banner) with auto-processing"""
|
"""Upload asset (logo/banner/avatar) with auto-processing. Accepts any image format."""
|
||||||
try:
|
try:
|
||||||
# Validate type
|
# Validate type
|
||||||
if type not in ['microdao_logo', 'microdao_banner', 'room_logo', 'room_banner']:
|
valid_types = ['microdao_logo', 'microdao_banner', 'room_logo', 'room_banner', 'agent_avatar']
|
||||||
raise HTTPException(status_code=400, detail="Invalid asset type")
|
if type not in valid_types:
|
||||||
|
raise HTTPException(status_code=400, detail=f"Invalid asset type. Valid: {valid_types}")
|
||||||
|
|
||||||
# Validate file size (5MB limit) - done by reading content
|
# Validate file size (20MB limit)
|
||||||
content = await file.read()
|
content = await file.read()
|
||||||
if len(content) > 5 * 1024 * 1024:
|
if len(content) > 20 * 1024 * 1024:
|
||||||
raise HTTPException(status_code=400, detail="File too large (max 5MB)")
|
raise HTTPException(status_code=400, detail="File too large (max 20MB)")
|
||||||
|
|
||||||
# Process image
|
# Process image based on type
|
||||||
target_size = (256, 256)
|
# Logos and avatars: square, max 512px
|
||||||
|
# Banners: max 1920px width, preserve aspect ratio
|
||||||
if 'banner' in type:
|
if 'banner' in type:
|
||||||
target_size = (1200, 400) # Standard banner size
|
max_size = 1920
|
||||||
|
force_square = False
|
||||||
|
else:
|
||||||
|
max_size = 512
|
||||||
|
force_square = True # Square crop for logos/avatars
|
||||||
|
|
||||||
processed_bytes, thumb_bytes = process_image(content, target_size=target_size)
|
processed_bytes, thumb_bytes = process_image(content, max_size=max_size, force_square=force_square)
|
||||||
|
|
||||||
# Save to disk
|
# Save to disk
|
||||||
filename = f"{uuid.uuid4()}.png"
|
filename = f"{uuid.uuid4()}.png"
|
||||||
@@ -4078,9 +4075,10 @@ async def get_node_swapper_detail(node_id: str):
|
|||||||
models = [
|
models = [
|
||||||
SwapperModel(
|
SwapperModel(
|
||||||
name=m.get("name", "unknown"),
|
name=m.get("name", "unknown"),
|
||||||
loaded=m.get("loaded", False),
|
# Swapper uses "status": "loaded" not "loaded": true
|
||||||
|
loaded=m.get("status") == "loaded" or m.get("loaded", False),
|
||||||
type=m.get("type"),
|
type=m.get("type"),
|
||||||
vram_gb=m.get("vram_gb")
|
vram_gb=m.get("size_gb") or m.get("vram_gb")
|
||||||
)
|
)
|
||||||
for m in models_data
|
for m in models_data
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -161,3 +161,4 @@ async def agents_presence_generator():
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user