|
|
|
|
@@ -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")
|
|
|
|
|
|
|
|
|
|
# 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:
|
|
|
|
|
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
|
|
|
|
|
]
|
|
|
|
|
|