"""SVG 아이콘 생성 — 텍스트 설명→SVG 코드 자동 생성""" from __future__ import annotations import json, logging, re from datetime import datetime from pathlib import Path from typing import Optional import httpx from fastapi import APIRouter, Depends, HTTPException from fastapi.responses import Response from pydantic import BaseModel from sqlalchemy import select, desc, func from sqlalchemy.ext.asyncio import AsyncSession from core.auth import get_current_user from database import get_db from models import User, GeneratedIcon logger = logging.getLogger(__name__) router = APIRouter(prefix="/api/icon", tags=["아이콘 생성"]) OLLAMA_URL = "http://localhost:11434" TEXT_MODEL = "llama3" # 내장 아이콘 템플릿 (GUARDiA 브랜드 #003366) BUILTIN_ICONS = { "server": '', "alert": '!', "check": '', "deploy": '', "user": '', "database": '', "network": '', "security": '', } SVG_SYSTEM_PROMPT = """당신은 SVG 아이콘 전문 디자이너입니다. 요구사항에 맞는 24x24 픽토그램 SVG 코드를 생성합니다. 규칙: 1. viewBox="0 0 24 24" 사용 2. xmlns="http://www.w3.org/2000/svg" 포함 3. 기본 색상: fill="{color}" 4. 단순하고 명확한 픽토그램 스타일 5. SVG 코드만 출력 (다른 텍스트 없음) 6. 반드시 완전한 SVG 태그로 시작하고 끝내기""" async def _generate_svg(description: str, color: str = "#003366", size: int = 24) -> str: """Ollama로 SVG 아이콘 생성.""" system = SVG_SYSTEM_PROMPT.replace("{color}", color) prompt = f"다음 아이콘을 SVG로 생성 (viewBox 0 0 {size} {size}): {description}" try: async with httpx.AsyncClient(timeout=30) as c: r = await c.post(f"{OLLAMA_URL}/api/generate", json={ "model": TEXT_MODEL, "system": system, "prompt": prompt, "stream": False, }) resp = r.json().get("response", "") # SVG 추출 svg_match = re.search(r'()', resp, re.IGNORECASE) if svg_match: return svg_match.group(1) # 코드 블록에서 추출 block_match = re.search(r'```(?:svg|xml)?\n?([\s\S]+?)```', resp) if block_match: candidate = block_match.group(1).strip() if ' str: """폴백: 텍스트 이니셜 SVG.""" initial = name[0].upper() if name else "?" return (f'' f'' f'{initial}') class IconRequest(BaseModel): description: str color: str = "#003366" size: int = 24 category: str = "custom" save: bool = True class BatchRequest(BaseModel): icons: list[IconRequest] @router.post("/generate", status_code=201) async def generate_icon(body: IconRequest, db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user)): """텍스트 설명→SVG 아이콘 생성.""" # 내장 아이콘 확인 for builtin_name, builtin_svg in BUILTIN_ICONS.items(): if builtin_name in body.description.lower(): return {"icon_id": f"builtin-{builtin_name}", "name": builtin_name, "svg_code": builtin_svg, "source": "builtin", "download_url": f"/api/icon/builtin/{builtin_name}"} svg = await _generate_svg(body.description, body.color, body.size) if not body.save: return {"svg_code": svg, "source": "generated"} icon = GeneratedIcon( name=body.description[:50], description=body.description, svg_code=svg, category=body.category, primary_color=body.color, size=body.size, created_by=user.id, created_at=datetime.utcnow(), ) db.add(icon); await db.commit(); await db.refresh(icon) return { "icon_id": icon.id, "name": icon.name, "svg_code": svg, "source": "ai_generated", "model": TEXT_MODEL, "download_url": f"/api/icon/{icon.id}/download", } @router.post("/batch") async def batch_generate(body: BatchRequest, db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user)): """여러 아이콘 일괄 생성.""" results = [] for req in body.icons[:10]: # 최대 10개 svg = await _generate_svg(req.description, req.color, req.size) icon = GeneratedIcon( name=req.description[:50], description=req.description, svg_code=svg, category=req.category, primary_color=req.color, size=req.size, created_by=user.id, created_at=datetime.utcnow(), ) db.add(icon) results.append({"description": req.description, "svg_preview": svg[:100] + "..."}) await db.commit() return {"generated": len(results), "icons": results} @router.get("/library") async def icon_library(db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user)): # 내장 아이콘 builtins = [{"id": f"builtin-{k}", "name": k, "source": "builtin", "download_url": f"/api/icon/builtin/{k}"} for k in BUILTIN_ICONS] # 생성 아이콘 rows = await db.execute( select(GeneratedIcon).order_by(desc(GeneratedIcon.created_at)).limit(50) ) custom = [{"id": ic.id, "name": ic.name, "category": ic.category, "source": "ai_generated", "download_url": f"/api/icon/{ic.id}/download"} for ic in rows.scalars().all()] return {"builtin": builtins, "custom": custom, "total": len(builtins) + len(custom)} @router.get("/builtin/{name}") async def get_builtin(name: str): """내장 아이콘 SVG 반환.""" if name not in BUILTIN_ICONS: raise HTTPException(404, f"내장 아이콘 '{name}' 없음") return Response(content=BUILTIN_ICONS[name], media_type="image/svg+xml", headers={"Content-Disposition": f'attachment; filename="{name}.svg"'}) @router.get("/{icon_id}/download") async def download_icon(icon_id: int, db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user)): row = await db.execute(select(GeneratedIcon).where(GeneratedIcon.id == icon_id)) icon = row.scalar_one_or_none() if not icon: raise HTTPException(404) return Response(content=icon.svg_code, media_type="image/svg+xml", headers={"Content-Disposition": f'attachment; filename="{icon.name}.svg"'}) @router.post("/customize/{icon_id}") async def customize_icon(icon_id: int, color: str = "#003366", size: int = 24, db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user)): """색상·크기 변경.""" row = await db.execute(select(GeneratedIcon).where(GeneratedIcon.id == icon_id)) icon = row.scalar_one_or_none() if not icon: raise HTTPException(404) customized = re.sub(r'fill="[^"]*"', f'fill="{color}"', icon.svg_code) return {"svg_code": customized, "color": color, "size": size}