diff --git a/apps/web/src/components/agent-dashboard/AgentAvatarUpload.tsx b/apps/web/src/components/agent-dashboard/AgentAvatarUpload.tsx index b8b9ebad..9f375b0e 100644 --- a/apps/web/src/components/agent-dashboard/AgentAvatarUpload.tsx +++ b/apps/web/src/components/agent-dashboard/AgentAvatarUpload.tsx @@ -158,7 +158,7 @@ export function AgentAvatarUpload({ {canEdit && (

- Upload a custom avatar for this agent. Recommended size: 256x256px. + Upload a custom avatar for this agent. Any image format accepted.

tuple[bytes, bytes]: +def process_image(image_bytes: bytes, max_size: int = 1024, force_square: bool = False) -> tuple[bytes, bytes]: """ Process image: - 1. Convert to PNG - 2. Resize/Crop to target_size (default 256x256) - 3. Generate thumbnail 128x128 + 1. Convert to PNG (any format accepted) + 2. Resize to fit within max_size (preserving aspect ratio) + 3. Optionally force square crop for avatars/logos + 4. Generate thumbnail 128x128 Returns (processed_bytes, thumb_bytes) """ with Image.open(io.BytesIO(image_bytes)) as img: # Convert to RGBA/RGB - if img.mode in ('P', 'CMYK'): + if img.mode in ('P', 'CMYK', 'LA'): img = img.convert('RGBA') + elif img.mode != 'RGBA': + img = img.convert('RGB') - # Resize/Crop to target_size - img_ratio = img.width / img.height - target_ratio = target_size[0] / target_size[1] - - if img_ratio > target_ratio: - # Wider than target - 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) + # Force square crop if needed (for avatars/logos) + if force_square: + min_dim = min(img.width, img.height) + left = (img.width - min_dim) / 2 + top = (img.height - min_dim) / 2 + img = img.crop((left, top, left + min_dim, top + min_dim)) - img = img.resize((new_width, new_height), Image.Resampling.LANCZOS) - - # Center crop - 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)) + # Resize to fit within max_size (preserve aspect ratio) + if img.width > max_size or img.height > max_size: + img.thumbnail((max_size, max_size), Image.Resampling.LANCZOS) # Save processed processed_io = io.BytesIO() @@ -318,25 +309,31 @@ async def update_agent_visibility_endpoint( @router.post("/assets/upload") async def upload_asset( 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: # Validate type - if type not in ['microdao_logo', 'microdao_banner', 'room_logo', 'room_banner']: - raise HTTPException(status_code=400, detail="Invalid asset type") + valid_types = ['microdao_logo', 'microdao_banner', 'room_logo', 'room_banner', 'agent_avatar'] + 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() - if len(content) > 5 * 1024 * 1024: - raise HTTPException(status_code=400, detail="File too large (max 5MB)") + if len(content) > 20 * 1024 * 1024: + raise HTTPException(status_code=400, detail="File too large (max 20MB)") - # Process image - target_size = (256, 256) + # Process image based on type + # Logos and avatars: square, max 512px + # Banners: max 1920px width, preserve aspect ratio 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 filename = f"{uuid.uuid4()}.png" @@ -4078,9 +4075,10 @@ async def get_node_swapper_detail(node_id: str): models = [ SwapperModel( 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"), - vram_gb=m.get("vram_gb") + vram_gb=m.get("size_gb") or m.get("vram_gb") ) for m in models_data ] diff --git a/services/city-service/websocket.py b/services/city-service/websocket.py index ed4d030f..642e190d 100644 --- a/services/city-service/websocket.py +++ b/services/city-service/websocket.py @@ -161,3 +161,4 @@ async def agents_presence_generator(): +