Merge pull request #7 from won-ktds/feature/local-to-azure

Feature/local to azure
This commit is contained in:
SeongRak Oh 2025-06-16 15:48:20 +09:00 committed by GitHub
commit 7748f96658
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 264 additions and 741 deletions

View File

@ -9,10 +9,9 @@ import os
from datetime import datetime
import traceback
from config.config import Config
from services.poster_service import PosterService
from services.sns_content_service import SnsContentService
from services.poster_service import PosterService
from models.request_models import ContentRequest, PosterRequest, SnsContentGetRequest, PosterContentGetRequest
from services.poster_service_v3 import PosterServiceV3
def create_app():
@ -30,7 +29,6 @@ def create_app():
# 서비스 인스턴스 생성
poster_service = PosterService()
poster_service_v3 = PosterServiceV3()
sns_content_service = SnsContentService()
@app.route('/health', methods=['GET'])
@ -149,12 +147,11 @@ def create_app():
)
# 포스터 생성 (V3 사용)
result = poster_service_v3.generate_poster(poster_request)
result = poster_service.generate_poster(poster_request)
if result['success']:
return jsonify({
'content': result['content'],
'analysis': result.get('analysis', {})
})
else:
return jsonify({'error': result['error']}), 500

View File

@ -12,14 +12,23 @@ class Config:
"""애플리케이션 설정 클래스"""
# Flask 기본 설정
SECRET_KEY = os.environ.get('SECRET_KEY') or 'dev-secret-key-change-in-production'
# 파일 업로드 설정
UPLOAD_FOLDER = os.environ.get('UPLOAD_FOLDER') or 'uploads'
MAX_CONTENT_LENGTH = int(os.environ.get('MAX_CONTENT_LENGTH') or 16 * 1024 * 1024) # 16MB
MAX_CONTENT_LENGTH = int(os.environ.get('MAX_CONTENT_LENGTH') or 16 * 1024 * 1536) # 16MB
# AI API 설정
CLAUDE_API_KEY = os.environ.get('CLAUDE_API_KEY')
OPENAI_API_KEY = os.environ.get('OPENAI_API_KEY')
# Azure Blob Storage 설정
AZURE_STORAGE_ACCOUNT_NAME = os.environ.get('AZURE_STORAGE_ACCOUNT_NAME') or 'stdigitalgarage02'
AZURE_STORAGE_ACCOUNT_KEY = os.environ.get('AZURE_STORAGE_ACCOUNT_KEY')
AZURE_STORAGE_CONTAINER_NAME = os.environ.get('AZURE_STORAGE_CONTAINER_NAME') or 'ai-content'
# 지원되는 파일 확장자
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'webp'}
# 템플릿 설정
POSTER_TEMPLATE_PATH = 'templates/poster_templates'

View File

@ -5,4 +5,5 @@ requests==2.31.0
anthropic>=0.25.0
openai>=1.12.0
python-dotenv==1.0.0
Werkzeug==3.0.1
Werkzeug==3.0.1
azure-storage-blob>=12.19.0

View File

@ -1,23 +1,32 @@
"""
포스터 생성 서비스
OpenAI사용한 이미지 생성 (한글 프롬프트)
포스터 생성 서비스 V3
OpenAI DALL-E사용한 이미지 생성 (메인 메뉴 이미지 1 + 프롬프트 예시 링크 10)
"""
import os
from typing import Dict, Any
from datetime import datetime
from typing import Dict, Any, List
from utils.ai_client import AIClient
from utils.image_processor import ImageProcessor
from models.request_models import PosterContentGetRequest
class PosterService:
"""포스터 생성 서비스 클래스"""
def __init__(self):
"""서비스 초기화"""
self.ai_client = AIClient()
self.image_processor = ImageProcessor()
# Azure Blob Storage 예시 이미지 링크 10개 (카페 음료 관련)
self.example_images = [
"https://stdigitalgarage02.blob.core.windows.net/ai-content/example1.png",
"https://stdigitalgarage02.blob.core.windows.net/ai-content/example2.png",
"https://stdigitalgarage02.blob.core.windows.net/ai-content/example3.png",
"https://stdigitalgarage02.blob.core.windows.net/ai-content/example4.png",
"https://stdigitalgarage02.blob.core.windows.net/ai-content/example5.png",
"https://stdigitalgarage02.blob.core.windows.net/ai-content/example6.png",
"https://stdigitalgarage02.blob.core.windows.net/ai-content/example7.png"
]
# 포토 스타일별 프롬프트
self.photo_styles = {
'미니멀': '미니멀하고 깔끔한 디자인, 단순함, 여백 활용',
@ -32,9 +41,7 @@ class PosterService:
self.category_styles = {
'음식': '음식 사진, 먹음직스러운, 맛있어 보이는',
'매장': '레스토랑 인테리어, 아늑한 분위기',
'이벤트': '홍보용 디자인, 눈길을 끄는',
'메뉴': '메뉴 디자인, 정리된 레이아웃',
'할인': '세일 포스터, 할인 디자인'
'이벤트': '홍보용 디자인, 눈길을 끄는'
}
# 톤앤매너별 디자인 스타일
@ -54,21 +61,27 @@ class PosterService:
def generate_poster(self, request: PosterContentGetRequest) -> Dict[str, Any]:
"""
포스터 생성 (OpenAI 이미지 URL 반환)
포스터 생성 (메인 이미지 1 분석 + 예시 링크 7 프롬프트 제공)
"""
try:
# 참조 이미지 분석 (있는 경우)
image_analysis = self._analyze_reference_images(request.images)
# 메인 이미지 확인
if not request.images:
return {'success': False, 'error': '메인 메뉴 이미지가 제공되지 않았습니다.'}
# 포스터 생성 프롬프트 생성
prompt = self._create_poster_prompt(request, image_analysis)
main_image_url = request.images[0] # 첫 번째 이미지가 메인 메뉴
# 메인 이미지 분석
main_image_analysis = self._analyze_main_image(main_image_url)
# 포스터 생성 프롬프트 생성 (예시 링크 10개 포함)
prompt = self._create_poster_prompt_v3(request, main_image_analysis)
# OpenAI로 이미지 생성
image_url = self.ai_client.generate_image_with_openai(prompt, "1024x1024")
image_url = self.ai_client.generate_image_with_openai(prompt, "1024x1536")
return {
'success': True,
'content': image_url
'content': image_url,
}
except Exception as e:
@ -77,117 +90,113 @@ class PosterService:
'error': str(e)
}
def _analyze_reference_images(self, image_urls: list) -> Dict[str, Any]:
def _analyze_main_image(self, image_url: str) -> Dict[str, Any]:
"""
참조 이미지들 분석
메인 메뉴 이미지 분석
"""
if not image_urls:
return {'total_images': 0, 'results': []}
analysis_results = []
temp_files = []
try:
for image_url in image_urls:
# 이미지 다운로드
temp_path = self.ai_client.download_image_from_url(image_url)
if temp_path:
temp_files.append(temp_path)
# 이미지 다운로드
temp_path = self.ai_client.download_image_from_url(image_url)
if temp_path:
temp_files.append(temp_path)
try:
# 이미지 분석
image_description = self.ai_client.analyze_image(temp_path)
# 색상 분석
colors = self.image_processor.analyze_colors(temp_path, 3)
# 이미지 분석
image_info = self.image_processor.get_image_info(temp_path)
image_description = self.ai_client.analyze_image(temp_path)
colors = self.image_processor.analyze_colors(temp_path, 5)
analysis_results.append({
'url': image_url,
'description': image_description,
'dominant_colors': colors
})
except Exception as e:
analysis_results.append({
'url': image_url,
'error': str(e)
})
return {
'url': image_url,
'info': image_info,
'description': image_description,
'dominant_colors': colors,
'is_food': self.image_processor.is_food_image(temp_path)
}
else:
return {
'url': image_url,
'error': '이미지 다운로드 실패'
}
except Exception as e:
return {
'total_images': len(image_urls),
'results': analysis_results
'url': image_url,
'error': str(e)
}
finally:
# 임시 파일 정리
for temp_file in temp_files:
try:
os.remove(temp_file)
except:
pass
def _create_poster_prompt(self, request: PosterContentGetRequest, image_analysis: Dict[str, Any]) -> str:
def _create_poster_prompt_v3(self, request: PosterContentGetRequest,
main_analysis: Dict[str, Any]) -> str:
"""
포스터 생성을 위한 AI 프롬프트 생성 (한글)
포스터 생성을 위한 AI 프롬프트 생성 (한글, 글자 완전 제외, 메인 이미지 기반 + 예시 링크 7 포함)
"""
# 기본 스타일 설정
photo_style = self.photo_styles.get(request.photoStyle, '현대적이고 깔끔한 디자인')
category_style = self.category_styles.get(request.category, '홍보용 디자인')
tone_style = self.tone_styles.get(request.toneAndManner, '친근하고 따뜻한 느낌')
emotion_design = self.emotion_designs.get(request.emotionIntensity, '적당히 활기찬 디자인')
# 참조 이미지 설명
reference_descriptions = []
for result in image_analysis.get('results', []):
if 'description' in result:
reference_descriptions.append(result['description'])
# 메인 이미지 정보 활용
main_description = main_analysis.get('description', '맛있는 음식')
main_colors = main_analysis.get('dominant_colors', [])
image_info = main_analysis.get('info', {})
# 색상 정보
color_info = ""
if image_analysis.get('results'):
colors = image_analysis['results'][0].get('dominant_colors', [])
if colors:
color_info = f"참조 색상 팔레트: {colors[:3]}을 활용한 조화로운 색감"
# 이미지 크기 및 비율 정보
aspect_ratio = image_info.get('aspect_ratio', 1.0) if image_info else 1.0
image_orientation = "가로형" if aspect_ratio > 1.2 else "세로형" if aspect_ratio < 0.8 else "정사각형"
# 색상 정보를 텍스트로 변환
color_description = ""
if main_colors:
color_rgb = main_colors[:3] # 상위 3개 색상
color_description = f"주요 색상 RGB 값: {color_rgb}를 기반으로 한 조화로운 색감"
# 예시 이미지 링크들을 문자열로 변환
example_links = "\n".join([f"- {link}" for link in self.example_images])
prompt = f"""
한국의 음식점/카페를 위한 전문적인 홍보 포스터를 디자인해주세요.
## 카페 홍보 포스터 디자인 요청
### 📋 기본 정보
카테고리: {request.category}
콘텐츠 타입: {request.contentType}
메뉴명: {request.menuName or '없음'}
메뉴 정보: {main_description}
### 📅 이벤트 기간
시작일: {request.startDate or '지금'}
종료일: {request.endDate or '한정 기간'}
이벤트 시작일과 종료일은 필수로 포스터에 명시해주세요.
### 🎨 디자인 요구사항
메인 이미지 처리
- 기존 메인 이미지는 변경하지 않고 그대로 유지
- 포스터 전체 크기의 1/3 이하로 배치
- 이미지와 조화로운 작은 장식 이미지 추가
- 크기: {image_orientation}
텍스트 요소
- 메뉴명 (필수)
- 간단한 추가 홍보 문구 (새로 생성, 한글) 혹은 "{request.requirement or '눈길을 끄는 전문적인 디자인'}"라는 요구사항에 맞는 문구
- 메뉴명 추가되는 문구는 1줄만 작성
텍스트 배치 규칙
- 글자가 이미지 경계를 벗어나지 않도록 주의
- 모서리에 너무 가깝게 배치하지
- 적당한 크기로 가독성 확보
- 아기자기한 한글 폰트 사용
### 🎨 디자인 스타일
참조 이미지
{example_links} URL을 참고하여 비슷한 스타일로 제작
색상 가이드
{color_description}
전체적인 디자인 방향
타겟: 한국 카페 고객층
스타일: 화려하고 매력적인 디자인
목적: 소셜미디어 공유용 (적합한 크기)
톤앤매너: 맛있어 보이는 색상, 방문 유도하는 비주얼
### 🎯 최종 목표
고객들이 "이 카페에 가보고 싶다!"라고 생각하게 만드는 시각적으로 매력적인 홍보 포스터 제작
"""
**메인 콘텐츠:**
- 제목: "{request.title}"
- 카테고리: {request.category}
- 콘텐츠 타입: {request.contentType}
**디자인 스타일 요구사항:**
- 포토 스타일: {photo_style}
- 카테고리 스타일: {category_style}
- 톤앤매너: {tone_style}
- 감정 강도: {emotion_design}
**메뉴 정보:**
- 메뉴명: {request.menuName or '없음'}
**이벤트 정보:**
- 이벤트명: {request.eventName or '특별 프로모션'}
- 시작일: {request.startDate or '지금'}
- 종료일: {request.endDate or '한정 기간'}
**특별 요구사항:**
{request.requirement or '눈길을 끄는 전문적인 디자인'}
**참조 이미지 설명:**
{chr(10).join(reference_descriptions) if reference_descriptions else '참조 이미지 없음'}
{color_info}
**디자인 가이드라인:**
- 한국 음식점/카페에 적합한 깔끔하고 현대적인 레이아웃
- 한글 텍스트 요소를 자연스럽게 포함
- 가독성이 좋은 전문적인 타이포그래피
- 명확한 대비로 읽기 쉽게 구성
- 소셜미디어 공유에 적합한 크기
- 저작권이 없는 오리지널 디자인
- 음식점에 어울리는 맛있어 보이는 색상 조합
- 고객의 시선을 끄는 매력적인 비주얼
고객들이 음식점을 방문하고 싶게 만드는 시각적으로 매력적인 포스터를 만들어주세요.
텍스트는 한글로, 전체적인 분위기는 한국적 감성에 맞게 디자인해주세요.
"""
return prompt

View File

@ -1,382 +0,0 @@
"""
하이브리드 포스터 생성 서비스
DALL-E: 텍스트 없는 아름다운 배경 생성
PIL: 완벽한 한글 텍스트 오버레이
"""
import os
from typing import Dict, Any
from datetime import datetime
from PIL import Image, ImageDraw, ImageFont, ImageEnhance
import requests
import io
from utils.ai_client import AIClient
from utils.image_processor import ImageProcessor
from models.request_models import PosterContentGetRequest
class PosterServiceV2:
"""하이브리드 포스터 생성 서비스"""
def __init__(self):
"""서비스 초기화"""
self.ai_client = AIClient()
self.image_processor = ImageProcessor()
def generate_poster(self, request: PosterContentGetRequest) -> Dict[str, Any]:
"""
하이브리드 포스터 생성
1. DALL-E로 텍스트 없는 배경 생성
2. PIL로 완벽한 한글 텍스트 오버레이
"""
try:
# 1. 참조 이미지 분석
image_analysis = self._analyze_reference_images(request.images)
# 2. DALL-E로 텍스트 없는 배경 생성
background_prompt = self._create_background_only_prompt(request, image_analysis)
background_url = self.ai_client.generate_image_with_openai(background_prompt, "1024x1024")
# 3. 배경 이미지 다운로드
background_image = self._download_and_load_image(background_url)
# 4. AI로 텍스트 컨텐츠 생성
text_content = self._generate_text_content(request)
# 5. PIL로 한글 텍스트 오버레이
final_poster = self._add_perfect_korean_text(background_image, text_content, request)
# 6. 최종 이미지 저장
poster_url = self._save_final_poster(final_poster)
return {
'success': True,
'content': poster_url
}
except Exception as e:
return {
'success': False,
'error': str(e)
}
def _create_background_only_prompt(self, request: PosterContentGetRequest, image_analysis: Dict[str, Any]) -> str:
"""텍스트 완전 제외 배경 전용 프롬프트"""
# 참조 이미지 설명
reference_descriptions = []
for result in image_analysis.get('results', []):
if 'description' in result:
reference_descriptions.append(result['description'])
prompt = f"""
Create a beautiful text-free background design for a Korean restaurant promotional poster.
ABSOLUTE REQUIREMENTS:
- NO TEXT, NO LETTERS, NO WORDS, NO CHARACTERS of any kind
- Pure visual background design only
- Professional Korean food business aesthetic
- Leave clear areas for text overlay (top 20% and bottom 30%)
DESIGN STYLE:
- Category: {request.category} themed design
- Photo Style: {request.photoStyle or 'modern'} aesthetic
- Mood: {request.toneAndManner or 'friendly'} atmosphere
- Intensity: {request.emotionIntensity or 'medium'} visual impact
VISUAL ELEMENTS TO INCLUDE:
- Korean traditional patterns or modern geometric designs
- Food-related visual elements (ingredients, cooking utensils, abstract food shapes)
- Warm, appetizing color palette
- Professional restaurant branding feel
- Clean, modern layout structure
REFERENCE CONTEXT:
{chr(10).join(reference_descriptions) if reference_descriptions else 'Clean, professional food business design'}
COMPOSITION:
- Central visual focus area
- Clear top section for main title
- Clear bottom section for details
- Balanced negative space
- High-end restaurant poster aesthetic
STRICTLY AVOID:
- Any form of text (Korean, English, numbers, symbols)
- Menu boards or signs with text
- Price displays
- Written content of any kind
- Typography elements
Create a premium, appetizing background that will make customers want to visit the restaurant.
Focus on visual appeal, color harmony, and professional food business branding.
"""
return prompt
def _download_and_load_image(self, image_url: str) -> Image.Image:
"""이미지 URL에서 PIL 이미지로 로드"""
response = requests.get(image_url, timeout=30)
response.raise_for_status()
return Image.open(io.BytesIO(response.content))
def _generate_text_content(self, request: PosterContentGetRequest) -> Dict[str, str]:
"""AI로 포스터 텍스트 컨텐츠 생성"""
prompt = f"""
한국 음식점 홍보 포스터용 텍스트를 생성해주세요.
포스터 정보:
- 제목: {request.title}
- 카테고리: {request.category}
- 메뉴명: {request.menuName or ''}
- 이벤트명: {request.eventName or ''}
- 시작일: {request.startDate or ''}
- 종료일: {request.endDate or ''}
다음 형식으로만 답변해주세요:
메인제목: [임팩트 있는 제목 8 이내]
서브제목: [설명 문구 15 이내]
기간정보: [기간 표시]
액션문구: [행동유도 8 이내]
"""
try:
ai_response = self.ai_client.generate_text(prompt, max_tokens=150)
return self._parse_text_content(ai_response, request)
except:
return self._create_fallback_content(request)
def _parse_text_content(self, ai_response: str, request: PosterContentGetRequest) -> Dict[str, str]:
"""AI 응답 파싱"""
content = {
'main_title': request.title[:8],
'sub_title': '',
'period_info': '',
'action_text': '지금 확인!'
}
lines = ai_response.split('\n')
for line in lines:
line = line.strip()
if '메인제목:' in line:
content['main_title'] = line.split('메인제목:')[1].strip()
elif '서브제목:' in line:
content['sub_title'] = line.split('서브제목:')[1].strip()
elif '기간정보:' in line:
content['period_info'] = line.split('기간정보:')[1].strip()
elif '액션문구:' in line:
content['action_text'] = line.split('액션문구:')[1].strip()
return content
def _create_fallback_content(self, request: PosterContentGetRequest) -> Dict[str, str]:
"""AI 실패시 기본 컨텐츠"""
return {
'main_title': request.title[:8] if request.title else '특별 이벤트',
'sub_title': request.eventName or request.menuName or '맛있는 음식',
'period_info': f"{request.startDate} ~ {request.endDate}" if request.startDate and request.endDate else '',
'action_text': '지금 방문!'
}
def _add_perfect_korean_text(self, background: Image.Image, content: Dict[str, str], request: PosterContentGetRequest) -> Image.Image:
"""완벽한 한글 텍스트 오버레이"""
# 배경 이미지 복사
poster = background.copy()
draw = ImageDraw.Draw(poster)
width, height = poster.size
# 한글 폰트 로드 (여러 경로 시도)
fonts = self._load_korean_fonts()
# 텍스트 색상 결정 (배경 분석 기반)
text_color = self._determine_text_color(background)
shadow_color = (0, 0, 0) if text_color == (255, 255, 255) else (255, 255, 255)
# 1. 메인 제목 (상단)
if content['main_title']:
self._draw_text_with_effects(
draw, content['main_title'],
fonts['title'], text_color, shadow_color,
width // 2, height * 0.15, 'center'
)
# 2. 서브 제목
if content['sub_title']:
self._draw_text_with_effects(
draw, content['sub_title'],
fonts['subtitle'], text_color, shadow_color,
width // 2, height * 0.75, 'center'
)
# 3. 기간 정보
if content['period_info']:
self._draw_text_with_effects(
draw, content['period_info'],
fonts['small'], text_color, shadow_color,
width // 2, height * 0.82, 'center'
)
# 4. 액션 문구 (강조 배경)
if content['action_text']:
self._draw_call_to_action(
draw, content['action_text'],
fonts['subtitle'], width, height
)
return poster
def _load_korean_fonts(self) -> Dict[str, ImageFont.FreeTypeFont]:
"""한글 폰트 로드 (여러 경로 시도)"""
font_paths = [
"/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",
"/System/Library/Fonts/Arial.ttf", # macOS
"C:/Windows/Fonts/arial.ttf", # Windows
"/usr/share/fonts/TTF/arial.ttf" # Linux
]
fonts = {}
for font_path in font_paths:
try:
fonts['title'] = ImageFont.truetype(font_path, 60)
fonts['subtitle'] = ImageFont.truetype(font_path, 32)
fonts['small'] = ImageFont.truetype(font_path, 24)
break
except:
continue
# 폰트 로드 실패시 기본 폰트
if not fonts:
fonts = {
'title': ImageFont.load_default(),
'subtitle': ImageFont.load_default(),
'small': ImageFont.load_default()
}
return fonts
def _determine_text_color(self, image: Image.Image) -> tuple:
"""배경 이미지 분석하여 텍스트 색상 결정"""
# 이미지 상단과 하단의 평균 밝기 계산
top_region = image.crop((0, 0, image.width, image.height // 4))
bottom_region = image.crop((0, image.height * 3 // 4, image.width, image.height))
def get_brightness(img_region):
grayscale = img_region.convert('L')
pixels = list(grayscale.getdata())
return sum(pixels) / len(pixels)
top_brightness = get_brightness(top_region)
bottom_brightness = get_brightness(bottom_region)
avg_brightness = (top_brightness + bottom_brightness) / 2
# 밝으면 검은색, 어두우면 흰색 텍스트
return (50, 50, 50) if avg_brightness > 128 else (255, 255, 255)
def _draw_text_with_effects(self, draw, text, font, color, shadow_color, x, y, align='center'):
"""그림자 효과가 있는 텍스트 그리기"""
if not text:
return
# 텍스트 크기 계산
bbox = draw.textbbox((0, 0), text, font=font)
text_width = bbox[2] - bbox[0]
text_height = bbox[3] - bbox[1]
# 위치 조정
if align == 'center':
x = x - text_width // 2
# 배경 박스 (가독성 향상)
padding = 10
box_coords = [
x - padding, y - padding,
x + text_width + padding, y + text_height + padding
]
draw.rectangle(box_coords, fill=(0, 0, 0, 180))
# 그림자 효과
shadow_offset = 2
draw.text((x + shadow_offset, y + shadow_offset), text, fill=shadow_color, font=font)
# 메인 텍스트
draw.text((x, y), text, fill=color, font=font)
def _draw_call_to_action(self, draw, text, font, width, height):
"""강조된 액션 버튼 스타일 텍스트"""
if not text:
return
# 버튼 위치 (하단 중앙)
button_y = height * 0.88
# 텍스트 크기
bbox = draw.textbbox((0, 0), text, font=font)
text_width = bbox[2] - bbox[0]
text_height = bbox[3] - bbox[1]
# 버튼 배경
button_width = text_width + 40
button_height = text_height + 20
button_x = (width - button_width) // 2
# 버튼 그리기
button_coords = [
button_x, button_y - 10,
button_x + button_width, button_y + button_height
]
draw.rounded_rectangle(button_coords, radius=25, fill=(255, 107, 107))
# 텍스트 그리기
text_x = (width - text_width) // 2
text_y = button_y + 5
draw.text((text_x, text_y), text, fill=(255, 255, 255), font=font)
def _save_final_poster(self, poster: Image.Image) -> str:
"""최종 포스터 저장"""
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
filename = f"hybrid_poster_{timestamp}.png"
filepath = os.path.join('uploads', 'temp', filename)
os.makedirs(os.path.dirname(filepath), exist_ok=True)
poster.save(filepath, 'PNG', quality=95)
return f"http://localhost:5001/uploads/temp/{filename}"
def _analyze_reference_images(self, image_urls: list) -> Dict[str, Any]:
"""참조 이미지 분석 (기존 코드와 동일)"""
if not image_urls:
return {'total_images': 0, 'results': []}
analysis_results = []
temp_files = []
try:
for image_url in image_urls:
temp_path = self.ai_client.download_image_from_url(image_url)
if temp_path:
temp_files.append(temp_path)
try:
image_description = self.ai_client.analyze_image(temp_path)
colors = self.image_processor.analyze_colors(temp_path, 3)
analysis_results.append({
'url': image_url,
'description': image_description,
'dominant_colors': colors
})
except Exception as e:
analysis_results.append({
'url': image_url,
'error': str(e)
})
return {
'total_images': len(image_urls),
'results': analysis_results
}
finally:
for temp_file in temp_files:
try:
os.remove(temp_file)
except:
pass

View File

@ -1,220 +0,0 @@
"""
포스터 생성 서비스 V3
OpenAI DALL-E를 사용한 이미지 생성 (메인 메뉴 이미지 1 + 프롬프트 예시 링크 10)
"""
import os
from typing import Dict, Any, List
from datetime import datetime
from utils.ai_client import AIClient
from utils.image_processor import ImageProcessor
from models.request_models import PosterContentGetRequest
class PosterServiceV3:
"""포스터 생성 서비스 V3 클래스"""
def __init__(self):
"""서비스 초기화"""
self.ai_client = AIClient()
self.image_processor = ImageProcessor()
# Azure Blob Storage 예시 이미지 링크 10개 (카페 음료 관련)
self.example_images = [
"https://stdigitalgarage02.blob.core.windows.net/ai-content/example1.png",
"https://stdigitalgarage02.blob.core.windows.net/ai-content/example2.png",
"https://stdigitalgarage02.blob.core.windows.net/ai-content/example3.png",
"https://stdigitalgarage02.blob.core.windows.net/ai-content/example4.png",
"https://stdigitalgarage02.blob.core.windows.net/ai-content/example5.png",
"https://stdigitalgarage02.blob.core.windows.net/ai-content/example6.png",
"https://stdigitalgarage02.blob.core.windows.net/ai-content/example7.png"
]
# 포토 스타일별 프롬프트
self.photo_styles = {
'미니멀': '미니멀하고 깔끔한 디자인, 단순함, 여백 활용',
'모던': '현대적이고 세련된 디자인, 깔끔한 레이아웃',
'빈티지': '빈티지 느낌, 레트로 스타일, 클래식한 색감',
'컬러풀': '다채로운 색상, 밝고 생동감 있는 컬러',
'우아한': '우아하고 고급스러운 느낌, 세련된 분위기',
'캐주얼': '친근하고 편안한 느낌, 접근하기 쉬운 디자인'
}
# 카테고리별 이미지 스타일
self.category_styles = {
'음식': '음식 사진, 먹음직스러운, 맛있어 보이는',
'매장': '레스토랑 인테리어, 아늑한 분위기',
'이벤트': '홍보용 디자인, 눈길을 끄는',
'메뉴': '메뉴 디자인, 정리된 레이아웃',
'할인': '세일 포스터, 할인 디자인',
'음료': '시원하고 상쾌한, 맛있어 보이는 음료'
}
# 톤앤매너별 디자인 스타일
self.tone_styles = {
'친근한': '따뜻하고 친근한 색감, 부드러운 느낌',
'정중한': '격식 있고 신뢰감 있는 디자인',
'재미있는': '밝고 유쾌한 분위기, 활기찬 색상',
'전문적인': '전문적이고 신뢰할 수 있는 디자인'
}
# 감정 강도별 디자인
self.emotion_designs = {
'약함': '은은하고 차분한 색감, 절제된 표현',
'보통': '적당히 활기찬 색상, 균형잡힌 디자인',
'강함': '강렬하고 임팩트 있는 색상, 역동적인 디자인'
}
def generate_poster(self, request: PosterContentGetRequest) -> Dict[str, Any]:
"""
포스터 생성 (메인 이미지 1 분석 + 예시 링크 10 프롬프트 제공)
"""
try:
# 메인 이미지 확인
if not request.images:
return {'success': False, 'error': '메인 메뉴 이미지가 제공되지 않았습니다.'}
main_image_url = request.images[0] # 첫 번째 이미지가 메인 메뉴
# 메인 이미지 분석
main_image_analysis = self._analyze_main_image(main_image_url)
# 포스터 생성 프롬프트 생성 (예시 링크 10개 포함)
prompt = self._create_poster_prompt_v3(request, main_image_analysis)
# OpenAI로 이미지 생성
image_url = self.ai_client.generate_image_with_openai(prompt, "1024x1536")
return {
'success': True,
'content': image_url,
'analysis': {
'main_image': main_image_analysis,
'example_images_used': len(self.example_images)
}
}
except Exception as e:
return {
'success': False,
'error': str(e)
}
def _analyze_main_image(self, image_url: str) -> Dict[str, Any]:
"""
메인 메뉴 이미지 분석
"""
temp_files = []
try:
# 이미지 다운로드
temp_path = self.ai_client.download_image_from_url(image_url)
if temp_path:
temp_files.append(temp_path)
# 이미지 분석
image_info = self.image_processor.get_image_info(temp_path)
image_description = self.ai_client.analyze_image(temp_path)
colors = self.image_processor.analyze_colors(temp_path, 5)
return {
'url': image_url,
'info': image_info,
'description': image_description,
'dominant_colors': colors,
'is_food': self.image_processor.is_food_image(temp_path)
}
else:
return {
'url': image_url,
'error': '이미지 다운로드 실패'
}
except Exception as e:
return {
'url': image_url,
'error': str(e)
}
finally:
# 임시 파일 정리
for temp_file in temp_files:
try:
os.remove(temp_file)
except:
pass
def _create_poster_prompt_v3(self, request: PosterContentGetRequest,
main_analysis: Dict[str, Any]) -> str:
"""
V3 포스터 생성을 위한 AI 프롬프트 생성 (한글, 글자 완전 제외, 메인 이미지 기반 + 예시 링크 10 포함)
"""
# 메인 이미지 정보 활용
main_description = main_analysis.get('description', '맛있는 음식')
main_colors = main_analysis.get('dominant_colors', [])
main_image_url = main_analysis.get('url', '')
image_info = main_analysis.get('info', {})
is_food = main_analysis.get('is_food', False)
# 이미지 크기 및 비율 정보
aspect_ratio = image_info.get('aspect_ratio', 1.0) if image_info else 1.0
image_orientation = "가로형" if aspect_ratio > 1.2 else "세로형" if aspect_ratio < 0.8 else "정사각형"
# 색상 정보를 텍스트로 변환
color_description = ""
if main_colors:
color_rgb = main_colors[:3] # 상위 3개 색상
color_description = f"주요 색상 RGB 값: {color_rgb}를 기반으로 한 조화로운 색감"
# 예시 이미지 링크들을 문자열로 변환
example_links = "\n".join([f"- {link}" for link in self.example_images])
prompt = f"""
## 카페 홍보 포스터 디자인 요청
### 📋 기본 정보
카테고리: {request.category}
콘텐츠 타입: {request.contentType}
메뉴명: {request.menuName or '없음'}
메뉴 정보: {main_description}
### 📅 이벤트 기간
시작일: {request.startDate or '지금'}
종료일: {request.endDate or '한정 기간'}
이벤트 시작일과 종료일은 필수로 포스터에 명시해주세요.
### 🎨 디자인 요구사항
메인 이미지 처리
- 기존 메인 이미지는 변경하지 않고 그대로 유지
- 포스터 전체 크기의 1/3 이하로 배치
- 이미지와 조화로운 작은 장식 이미지 추가
- 크기: {image_orientation}
텍스트 요소
- 메뉴명 (필수)
- 간단한 추가 홍보 문구 (새로 생성, 한글) 혹은 "{request.requirement or '눈길을 끄는 전문적인 디자인'}"라는 요구사항에 맞는 문구
- 메뉴명 추가되는 문구는 1줄만 작성
텍스트 배치 규칙
- 글자가 이미지 경계를 벗어나지 않도록 주의
- 모서리에 너무 가깝게 배치하지
- 적당한 크기로 가독성 확보
- 아기자기한 한글 폰트 사용
### 🎨 디자인 스타일
참조 이미지
{example_links} URL을 참고하여 비슷한 스타일로 제작
색상 가이드
{color_description}
전체적인 디자인 방향
타겟: 한국 카페 고객층
스타일: 화려하고 매력적인 디자인
목적: 소셜미디어 공유용 (적합한 크기)
톤앤매너: 맛있어 보이는 색상, 방문 유도하는 비주얼
### 🎯 최종 목표
고객들이 "이 카페에 가보고 싶다!"라고 생각하게 만드는 시각적으로 매력적인 홍보 포스터 제작
"""
return prompt

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 MiB

View File

@ -10,6 +10,7 @@ import anthropic
import openai
from PIL import Image
import io
from utils.blob_storage import BlobStorageClient
class AIClient:
@ -20,6 +21,9 @@ class AIClient:
self.claude_api_key = os.getenv('CLAUDE_API_KEY')
self.openai_api_key = os.getenv('OPENAI_API_KEY')
# Blob Storage 클라이언트 초기화
self.blob_client = BlobStorageClient()
# Claude 클라이언트 초기화
if self.claude_api_key:
self.claude_client = anthropic.Anthropic(api_key=self.claude_api_key)
@ -64,14 +68,14 @@ class AIClient:
print(f"이미지 다운로드 실패 {image_url}: {e}")
return None
def generate_image_with_openai(self, prompt: str, size: str = "1024x1024") -> str:
def generate_image_with_openai(self, prompt: str, size: str = "1024x1536") -> str:
"""
OpenAI DALL-E사용하여 이미지 생성
gpt사용하여 이미지 생성
Args:
prompt: 이미지 생성 프롬프트
size: 이미지 크기 (1024x1024, 1792x1024, 1024x1792)
size: 이미지 크기 (1024x1536)
Returns:
생성이미지 URL
Azure Blob Storage에 저장이미지 URL
"""
try:
if not self.openai_client:
@ -84,27 +88,15 @@ class AIClient:
n=1,
)
# base64를 파일로 저장
import base64
from datetime import datetime
# base64 이미지 데이터 추출
b64_data = response.data[0].b64_json
image_data = base64.b64decode(b64_data)
# 로컬 파일 저장
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
filename = f"poster_{timestamp}.png"
filepath = os.path.join('uploads', 'temp', filename)
# Azure Blob Storage에 업로드
blob_url = self.blob_client.upload_image(image_data, 'png')
os.makedirs(os.path.dirname(filepath), exist_ok=True)
with open(filepath, 'wb') as f:
f.write(image_data)
print(f"✅ 이미지 저장 완료: {filepath}")
# 그냥 파일 경로만 반환
return filepath
print(f"✅ 이미지 생성 및 업로드 완료: {blob_url}")
return blob_url
except Exception as e:
raise Exception(f"이미지 생성 실패: {str(e)}")

View File

@ -0,0 +1,117 @@
"""
Azure Blob Storage 유틸리티
이미지 업로드 URL 생성 기능 제공
"""
import os
from datetime import datetime
from typing import Optional
from azure.storage.blob import BlobServiceClient, ContentSettings
from config.config import Config
class BlobStorageClient:
"""Azure Blob Storage 클라이언트 클래스"""
def __init__(self):
"""Blob Storage 클라이언트 초기화"""
self.account_name = Config.AZURE_STORAGE_ACCOUNT_NAME
self.account_key = Config.AZURE_STORAGE_ACCOUNT_KEY
self.container_name = Config.AZURE_STORAGE_CONTAINER_NAME
if not self.account_key:
raise ValueError("Azure Storage Account Key가 설정되지 않았습니다.")
# Connection String 생성
connection_string = f"DefaultEndpointsProtocol=https;AccountName={self.account_name};AccountKey={self.account_key};EndpointSuffix=core.windows.net"
# Blob Service Client 초기화
self.blob_service_client = BlobServiceClient.from_connection_string(connection_string)
def upload_image(self, image_data: bytes, file_extension: str = 'png') -> str:
"""
이미지를 Blob Storage에 업로드
Args:
image_data: 업로드할 이미지 바이트 데이터
file_extension: 파일 확장자 (기본값: 'png')
Returns:
업로드된 이미지의 Public URL
"""
try:
# 파일명 생성: poster_YYYYMMDDHHMMSS.png
timestamp = datetime.now().strftime('%Y%m%d%H%M%S')
blob_name = f"poster_{timestamp}.{file_extension}"
# Content Type 설정
content_settings = ContentSettings(content_type=f'image/{file_extension}')
# Blob 업로드
blob_client = self.blob_service_client.get_blob_client(
container=self.container_name,
blob=blob_name
)
blob_client.upload_blob(
image_data,
content_settings=content_settings,
overwrite=True
)
# Public URL 생성
blob_url = f"https://{self.account_name}.blob.core.windows.net/{self.container_name}/{blob_name}"
print(f"✅ 이미지 업로드 완료: {blob_url}")
return blob_url
except Exception as e:
print(f"❌ Blob Storage 업로드 실패: {str(e)}")
raise Exception(f"이미지 업로드 실패: {str(e)}")
def upload_file(self, file_path: str) -> str:
"""
로컬 파일을 Blob Storage에 업로드
Args:
file_path: 업로드할 로컬 파일 경로
Returns:
업로드된 파일의 Public URL
"""
try:
# 파일 확장자 추출
file_extension = os.path.splitext(file_path)[1][1:].lower()
# 파일 읽기
with open(file_path, 'rb') as file:
file_data = file.read()
# 업로드
return self.upload_image(file_data, file_extension)
except Exception as e:
print(f"❌ 파일 업로드 실패: {str(e)}")
raise Exception(f"파일 업로드 실패: {str(e)}")
def delete_blob(self, blob_name: str) -> bool:
"""
Blob 삭제
Args:
blob_name: 삭제할 Blob 이름
Returns:
삭제 성공 여부
"""
try:
blob_client = self.blob_service_client.get_blob_client(
container=self.container_name,
blob=blob_name
)
blob_client.delete_blob()
print(f"✅ Blob 삭제 완료: {blob_name}")
return True
except Exception as e:
print(f"❌ Blob 삭제 실패: {str(e)}")
return False