This commit is contained in:
박서은 2025-06-11 17:32:57 +09:00
commit 4464eb9fa1
9 changed files with 660 additions and 551 deletions

View File

@ -1,6 +0,0 @@
CLAUDE_API_KEY=your_claude_api_key_here
OPENAI_API_KEY=your_openai_api_key_here
FLASK_ENV=development
UPLOAD_FOLDER=uploads
MAX_CONTENT_LENGTH=16777216
SECRET_KEY=your-secret-key-for-production

View File

@ -1,2 +1,23 @@
.idea
# Python 가상환경
venv/
env/
ENV/
.venv/
.env/
# Python 캐시
__pycache__/
*.py[cod]
*$py.class
*.so
# 환경 변수 파일
.env
.env.local
.env.*.local
# IDE 설정
.vscode/
.idea/
*.swp
*.swo

View File

@ -1,4 +1,3 @@
# Dockerfile
FROM python:3.11-slim
WORKDIR /app

View File

@ -9,24 +9,29 @@ import os
from datetime import datetime
import traceback
from config.config import Config
from services.content_service import ContentService
# from services.content_service import ContentService
from services.poster_service import PosterService
from models.request_models import ContentRequest, PosterRequest
from services.sns_content_service import SnsContentService
# from services.poster_generation_service import PosterGenerationService
from models.request_models import ContentRequest, PosterRequest, SnsContentGetRequest, PosterContentGetRequest
def create_app():
"""Flask 애플리케이션 팩토리"""
app = Flask(__name__)
app.config.from_object(Config)
# CORS 설정
CORS(app)
# 업로드 폴더 생성
os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
os.makedirs(os.path.join(app.config['UPLOAD_FOLDER'], 'temp'), exist_ok=True)
os.makedirs('templates/poster_templates', exist_ok=True)
# 서비스 인스턴스 생성
content_service = ContentService()
poster_service = PosterService()
sns_content_service = SnsContentService()
@app.route('/health', methods=['GET'])
def health_check():
@ -37,16 +42,119 @@ def create_app():
'service': 'AI Marketing Service'
})
# ===== 새로운 API 엔드포인트 =====
@app.route('/api/ai/sns', methods=['POST'])
def generate_sns_content():
"""
SNS 게시물 생성 API (새로운 요구사항)
Java 서버에서 JSON 형태로 요청받아 HTML 형식의 게시물 반환
"""
try:
# JSON 요청 데이터 검증
if not request.is_json:
return jsonify({'error': 'Content-Type은 application/json이어야 합니다.'}), 400
data = request.get_json()
if not data:
return jsonify({'error': '요청 데이터가 없습니다.'}), 400
# 필수 필드 검증
required_fields = ['title', 'category', 'contentType', 'platform', 'images']
for field in required_fields:
if field not in data:
return jsonify({'error': f'필수 필드가 누락되었습니다: {field}'}), 400
# 요청 모델 생성
sns_request = SnsContentGetRequest(
title=data.get('title'),
category=data.get('category'),
contentType=data.get('contentType'),
platform=data.get('platform'),
images=data.get('images', []),
requirement=data.get('requirement'),
toneAndManner=data.get('toneAndManner'),
emotionIntensity=data.get('emotionIntensity'),
eventName=data.get('eventName'),
startDate=data.get('startDate'),
endDate=data.get('endDate')
)
# SNS 콘텐츠 생성
result = sns_content_service.generate_sns_content(sns_request)
if result['success']:
return jsonify({'content': result['content']})
else:
return jsonify({'error': result['error']}), 500
except Exception as e:
app.logger.error(f"SNS 콘텐츠 생성 중 오류 발생: {str(e)}")
app.logger.error(traceback.format_exc())
return jsonify({'error': f'SNS 콘텐츠 생성 중 오류가 발생했습니다: {str(e)}'}), 500
@app.route('/api/ai/poster', methods=['POST'])
def generate_poster_content():
"""
홍보 포스터 생성 API (새로운 요구사항)
Java 서버에서 JSON 형태로 요청받아 OpenAI 이미지 URL 반환
"""
try:
# JSON 요청 데이터 검증
if not request.is_json:
return jsonify({'error': 'Content-Type은 application/json이어야 합니다.'}), 400
data = request.get_json()
if not data:
return jsonify({'error': '요청 데이터가 없습니다.'}), 400
# 필수 필드 검증
required_fields = ['title', 'category', 'contentType', 'images']
for field in required_fields:
if field not in data:
return jsonify({'error': f'필수 필드가 누락되었습니다: {field}'}), 400
# 요청 모델 생성
poster_request = PosterContentGetRequest(
title=data.get('title'),
category=data.get('category'),
contentType=data.get('contentType'),
images=data.get('images', []),
photoStyle=data.get('photoStyle'),
requirement=data.get('requirement'),
toneAndManner=data.get('toneAndManner'),
emotionIntensity=data.get('emotionIntensity'),
eventName=data.get('eventName'),
startDate=data.get('startDate'),
endDate=data.get('endDate')
)
# 포스터 생성
result = poster_service.generate_poster(poster_request)
if result['success']:
return jsonify({'content': result['content']})
else:
return jsonify({'error': result['error']}), 500
except Exception as e:
app.logger.error(f"포스터 생성 중 오류 발생: {str(e)}")
app.logger.error(traceback.format_exc())
return jsonify({'error': f'포스터 생성 중 오류가 발생했습니다: {str(e)}'}), 500
# ===== 기존 API 엔드포인트 (하위 호환성) =====
@app.route('/api/content/generate', methods=['POST'])
def generate_content():
"""
마케팅 콘텐츠 생성 API
마케팅 콘텐츠 생성 API (기존)
점주가 입력한 정보를 바탕으로 플랫폼별 맞춤 게시글 생성
"""
try:
# 요청 데이터 검증
if not request.form:
return jsonify({'error': '요청 데이터가 없습니다.'}), 400
# 파일 업로드 처리
uploaded_files = []
if 'images' in request.files:
@ -59,6 +167,7 @@ def create_app():
file_path = os.path.join(app.config['UPLOAD_FOLDER'], 'temp', unique_filename)
file.save(file_path)
uploaded_files.append(file_path)
# 요청 모델 생성
content_request = ContentRequest(
category=request.form.get('category', '음식'),
@ -69,15 +178,19 @@ def create_app():
store_name=request.form.get('store_name', ''),
additional_info=request.form.get('additional_info', '')
)
# 콘텐츠 생성
result = content_service.generate_content(content_request)
result = sns_content_service.generate_content(content_request)
# 임시 파일 정리
for file_path in uploaded_files:
try:
os.remove(file_path)
except OSError:
pass
return jsonify(result)
except Exception as e:
# 에러 발생 시 임시 파일 정리
for file_path in uploaded_files:
@ -92,13 +205,14 @@ def create_app():
@app.route('/api/poster/generate', methods=['POST'])
def generate_poster():
"""
홍보 포스터 생성 API
홍보 포스터 생성 API (기존)
점주가 입력한 정보를 바탕으로 시각적 홍보 포스터 생성
"""
try:
# 요청 데이터 검증
if not request.form:
return jsonify({'error': '요청 데이터가 없습니다.'}), 400
# 파일 업로드 처리
uploaded_files = []
if 'images' in request.files:
@ -111,6 +225,7 @@ def create_app():
file_path = os.path.join(app.config['UPLOAD_FOLDER'], 'temp', unique_filename)
file.save(file_path)
uploaded_files.append(file_path)
# 요청 모델 생성
poster_request = PosterRequest(
category=request.form.get('category', '음식'),
@ -122,15 +237,19 @@ def create_app():
discount_info=request.form.get('discount_info', ''),
additional_info=request.form.get('additional_info', '')
)
# 포스터 생성
result = poster_service.generate_poster(poster_request)
# 임시 파일 정리
for file_path in uploaded_files:
try:
os.remove(file_path)
except OSError:
pass
return jsonify(result)
except Exception as e:
# 에러 발생 시 임시 파일 정리
for file_path in uploaded_files:

View File

@ -4,24 +4,61 @@ API 요청 데이터 구조를 정의
"""
from dataclasses import dataclass
from typing import List, Optional
@dataclass
class SnsContentGetRequest:
"""SNS 게시물 생성 요청 모델"""
title: str
category: str
contentType: str
platform: str
images: List[str] # 이미지 URL 리스트
requirement: Optional[str] = None
toneAndManner: Optional[str] = None
emotionIntensity: Optional[str] = None
eventName: Optional[str] = None
startDate: Optional[str] = None
endDate: Optional[str] = None
@dataclass
class PosterContentGetRequest:
"""홍보 포스터 생성 요청 모델"""
title: str
category: str
contentType: str
images: List[str] # 이미지 URL 리스트
photoStyle: Optional[str] = None
requirement: Optional[str] = None
toneAndManner: Optional[str] = None
emotionIntensity: Optional[str] = None
eventName: Optional[str] = None
startDate: Optional[str] = None
endDate: Optional[str] = None
# 기존 모델들은 유지
@dataclass
class ContentRequest:
"""마케팅 콘텐츠 생성 요청 모델"""
category: str # 음식, 매장, 이벤트
platform: str # 네이버 블로그, 인스타그램
image_paths: List[str] # 업로드된 이미지 파일 경로들
start_time: Optional[str] = None # 이벤트 시작 시간
end_time: Optional[str] = None # 이벤트 종료 시간
store_name: Optional[str] = None # 매장명
additional_info: Optional[str] = None # 추가 정보
"""마케팅 콘텐츠 생성 요청 모델 (기존)"""
category: str
platform: str
image_paths: List[str]
start_time: Optional[str] = None
end_time: Optional[str] = None
store_name: Optional[str] = None
additional_info: Optional[str] = None
@dataclass
class PosterRequest:
"""홍보 포스터 생성 요청 모델"""
category: str # 음식, 매장, 이벤트
image_paths: List[str] # 업로드된 이미지 파일 경로들
start_time: Optional[str] = None # 이벤트 시작 시간
end_time: Optional[str] = None # 이벤트 종료 시간
store_name: Optional[str] = None # 매장명
event_title: Optional[str] = None # 이벤트 제목
discount_info: Optional[str] = None # 할인 정보
additional_info: Optional[str] = None # 추가 정보
"""홍보 포스터 생성 요청 모델 (기존)"""
category: str
image_paths: List[str]
start_time: Optional[str] = None
end_time: Optional[str] = None
store_name: Optional[str] = None
event_title: Optional[str] = None
discount_info: Optional[str] = None
additional_info: Optional[str] = None

View File

@ -1,208 +0,0 @@
"""
마케팅 콘텐츠 생성 서비스
AI를 활용하여 플랫폼별 맞춤 게시글 생성
"""
import os
from typing import Dict, Any
from datetime import datetime
from utils.ai_client import AIClient
from utils.image_processor import ImageProcessor
from models.request_models import ContentRequest
class ContentService:
"""마케팅 콘텐츠 생성 서비스 클래스"""
def __init__(self):
"""서비스 초기화"""
self.ai_client = AIClient()
self.image_processor = ImageProcessor()
# 플랫폼별 콘텐츠 특성 정의
self.platform_specs = {
'인스타그램': {
'max_length': 2200,
'hashtag_count': 15,
'style': '감성적이고 시각적',
'format': '짧은 문장, 해시태그 활용'
},
'네이버 블로그': {
'max_length': 3000,
'hashtag_count': 10,
'style': '정보성과 친근함',
'format': '구조화된 내용, 상세 설명'
}
}
# 카테고리별 키워드 정의
self.category_keywords = {
'음식': ['맛집', '신메뉴', '추천', '맛있는', '특별한', '인기'],
'매장': ['분위기', '인테리어', '편안한', '아늑한', '특별한', '방문'],
'이벤트': ['할인', '이벤트', '특가', '한정', '기간한정', '혜택']
}
def generate_content(self, request: ContentRequest) -> Dict[str, Any]:
"""
마케팅 콘텐츠 생성
Args:
request: 콘텐츠 생성 요청 데이터
Returns:
생성된 콘텐츠 정보
"""
try:
# 이미지 분석
image_analysis = self._analyze_images(request.image_paths)
# AI 프롬프트 생성
prompt = self._create_content_prompt(request, image_analysis)
# AI로 콘텐츠 생성
generated_content = self.ai_client.generate_text(prompt)
# 해시태그 생성
hashtags = self._generate_hashtags(request)
# 최종 콘텐츠 포맷팅
formatted_content = self._format_content(
generated_content,
hashtags,
request.platform
)
return {
'success': True,
'content': formatted_content,
'platform': request.platform,
'category': request.category,
'generated_at': datetime.now().isoformat(),
'image_count': len(request.image_paths),
'image_analysis': image_analysis
}
except Exception as e:
return {
'success': False,
'error': str(e),
'generated_at': datetime.now().isoformat()
}
def _analyze_images(self, image_paths: list) -> Dict[str, Any]:
"""
업로드된 이미지들 분석
Args:
image_paths: 이미지 파일 경로 리스트
Returns:
이미지 분석 결과
"""
analysis_results = []
for image_path in image_paths:
try:
# 이미지 기본 정보 추출
image_info = self.image_processor.get_image_info(image_path)
# AI를 통한 이미지 내용 분석
image_description = self.ai_client.analyze_image(image_path)
analysis_results.append({
'path': image_path,
'info': image_info,
'description': image_description
})
except Exception as e:
analysis_results.append({
'path': image_path,
'error': str(e)
})
return {
'total_images': len(image_paths),
'results': analysis_results
}
def _create_content_prompt(self, request: ContentRequest, image_analysis: Dict[str, Any]) -> str:
"""
AI 콘텐츠 생성을 위한 프롬프트 생성
Args:
request: 콘텐츠 생성 요청
image_analysis: 이미지 분석 결과
Returns:
AI 프롬프트 문자열
"""
platform_spec = self.platform_specs.get(request.platform, self.platform_specs['인스타그램'])
category_keywords = self.category_keywords.get(request.category, [])
# 이미지 설명 추출
image_descriptions = []
for result in image_analysis.get('results', []):
if 'description' in result:
image_descriptions.append(result['description'])
prompt = f"""
당신은 소상공인을 위한 마케팅 콘텐츠 전문가입니다.
다음 정보를 바탕으로 {request.platform} 적합한 {request.category} 카테고리의 게시글을 작성해주세요.
**매장 정보:**
- 매장명: {request.store_name or '우리 가게'}
- 카테고리: {request.category}
- 추가 정보: {request.additional_info or '없음'}
**이벤트 정보:**
- 시작 시간: {request.start_time or '상시'}
- 종료 시간: {request.end_time or '상시'}
**이미지 분석 결과:**
{chr(10).join(image_descriptions) if image_descriptions else '이미지 없음'}
**플랫폼 특성:**
- 최대 길이: {platform_spec['max_length']}
- 스타일: {platform_spec['style']}
- 형식: {platform_spec['format']}
**요구사항:**
1. {request.platform} 특성에 맞는 톤앤매너 사용
2. {request.category} 카테고리에 적합한 내용 구성
3. 고객의 관심을 있는 매력적인 문구 사용
4. 이미지와 연관된 내용으로 작성
5. 자연스럽고 친근한 어조 사용
해시태그는 별도로 생성하므로 본문에는 포함하지 마세요.
"""
return prompt
def _generate_hashtags(self, request: ContentRequest) -> list:
"""
카테고리와 플랫폼에 맞는 해시태그 생성
Args:
request: 콘텐츠 생성 요청
Returns:
해시태그 리스트
"""
platform_spec = self.platform_specs.get(request.platform, self.platform_specs['인스타그램'])
category_keywords = self.category_keywords.get(request.category, [])
hashtags = []
# 기본 해시태그
if request.store_name:
hashtags.append(f"#{request.store_name.replace(' ', '')}")
# 카테고리별 해시태그
hashtags.extend([f"#{keyword}" for keyword in category_keywords[:5]])
# 공통 해시태그
common_tags = ['#맛집', '#소상공인', '#로컬맛집', '#일상', '#소통']
hashtags.extend(common_tags)
# 플랫폼별 인기 해시태그
if request.platform == '인스타그램':
hashtags.extend(['#인스타푸드', '#데일리', '#오늘뭐먹지', '#맛스타그램'])
elif request.platform == '네이버 블로그':
hashtags.extend(['#블로그', '#후기', '#추천', '#정보'])
# 최대 개수 제한
max_count = platform_spec['hashtag_count']
return hashtags[:max_count]
def _format_content(self, content: str, hashtags: list, platform: str) -> str:
"""
플랫폼에 맞게 콘텐츠 포맷팅
Args:
content: 생성된 콘텐츠
hashtags: 해시태그 리스트
platform: 플랫폼명
Returns:
포맷팅된 최종 콘텐츠
"""
platform_spec = self.platform_specs.get(platform, self.platform_specs['인스타그램'])
# 길이 제한 적용
if len(content) > platform_spec['max_length'] - 100: # 해시태그 공간 확보
content = content[:platform_spec['max_length'] - 100] + '...'
# 플랫폼별 포맷팅
if platform == '인스타그램':
# 인스타그램: 본문 + 해시태그
hashtag_string = ' '.join(hashtags)
formatted = f"{content}\n\n{hashtag_string}"
elif platform == '네이버 블로그':
# 네이버 블로그: 구조화된 형태
hashtag_string = ' '.join(hashtags)
formatted = f"{content}\n\n---\n{hashtag_string}"
else:
# 기본 형태
hashtag_string = ' '.join(hashtags)
formatted = f"{content}\n\n{hashtag_string}"
return formatted

View File

@ -1,312 +1,190 @@
"""
홍보 포스터 생성 서비스
AI와 이미지 처리를 활용한 시각적 마케팅 자료 생성
포스터 생성 서비스
OpenAI를 사용한 이미지 생성 (한글 프롬프트)
"""
import os
import base64
from typing import Dict, Any
from datetime import datetime
from PIL import Image, ImageDraw, ImageFont
from utils.ai_client import AIClient
from utils.image_processor import ImageProcessor
from models.request_models import PosterRequest
from models.request_models import PosterContentGetRequest
class PosterService:
"""홍보 포스터 생성 서비스 클래스"""
"""포스터 생성 서비스 클래스"""
def __init__(self):
"""서비스 초기화"""
self.ai_client = AIClient()
self.image_processor = ImageProcessor()
# 포스터 기본 설정
self.poster_config = {
'width': 1080,
'height': 1350, # 인스타그램 세로 비율
'background_color': (255, 255, 255),
'text_color': (50, 50, 50),
'accent_color': (255, 107, 107)
}
# 카테고리별 색상 테마
self.category_themes = {
'음식': {
'primary': (255, 107, 107), # 빨강
'secondary': (255, 206, 84), # 노랑
'background': (255, 248, 240) # 크림
},
'매장': {
'primary': (74, 144, 226), # 파랑
'secondary': (120, 198, 121), # 초록
'background': (248, 251, 255) # 연한 파랑
},
'이벤트': {
'primary': (156, 39, 176), # 보라
'secondary': (255, 193, 7), # 금색
'background': (252, 248, 255) # 연한 보라
}
# 포토 스타일별 프롬프트
self.photo_styles = {
'미니멀': '미니멀하고 깔끔한 디자인, 단순함, 여백 활용',
'모던': '현대적이고 세련된 디자인, 깔끔한 레이아웃',
'빈티지': '빈티지 느낌, 레트로 스타일, 클래식한 색감',
'컬러풀': '다채로운 색상, 밝고 생동감 있는 컬러',
'우아한': '우아하고 고급스러운 느낌, 세련된 분위기',
'캐주얼': '친근하고 편안한 느낌, 접근하기 쉬운 디자인'
}
def generate_poster(self, request: PosterRequest) -> Dict[str, Any]:
# 카테고리별 이미지 스타일
self.category_styles = {
'음식': '음식 사진, 먹음직스러운, 맛있어 보이는',
'매장': '레스토랑 인테리어, 아늑한 분위기',
'이벤트': '홍보용 디자인, 눈길을 끄는',
'메뉴': '메뉴 디자인, 정리된 레이아웃',
'할인': '세일 포스터, 할인 디자인'
}
# 톤앤매너별 디자인 스타일
self.tone_styles = {
'친근한': '따뜻하고 친근한 색감, 부드러운 느낌',
'정중한': '격식 있고 신뢰감 있는 디자인',
'재미있는': '밝고 유쾌한 분위기, 활기찬 색상',
'전문적인': '전문적이고 신뢰할 수 있는 디자인'
}
# 감정 강도별 디자인
self.emotion_designs = {
'약함': '은은하고 차분한 색감, 절제된 표현',
'보통': '적당히 활기찬 색상, 균형잡힌 디자인',
'강함': '강렬하고 임팩트 있는 색상, 역동적인 디자인'
}
def generate_poster(self, request: PosterContentGetRequest) -> Dict[str, Any]:
"""
홍보 포스터 생성
Args:
request: 포스터 생성 요청 데이터
Returns:
생성된 포스터 정보
포스터 생성 (OpenAI 이미지 URL 반환)
"""
try:
# 포스터 텍스트 내용 생성
poster_text = self._generate_poster_text(request)
# 이미지 전처리
processed_images = self._process_images(request.image_paths)
# 포스터 이미지 생성
poster_image = self._create_poster_image(request, poster_text, processed_images)
# 이미지를 base64로 인코딩
poster_base64 = self._encode_image_to_base64(poster_image)
# 참조 이미지 분석 (있는 경우)
image_analysis = self._analyze_reference_images(request.images)
# 포스터 생성 프롬프트 생성
prompt = self._create_poster_prompt(request, image_analysis)
# OpenAI로 이미지 생성
image_url = self.ai_client.generate_image_with_openai(prompt, "1024x1024")
return {
'success': True,
'poster_data': poster_base64,
'poster_text': poster_text,
'category': request.category,
'generated_at': datetime.now().isoformat(),
'image_count': len(request.image_paths),
'format': 'base64'
'content': image_url
}
except Exception as e:
return {
'success': False,
'error': str(e),
'generated_at': datetime.now().isoformat()
'error': str(e)
}
def _generate_poster_text(self, request: PosterRequest) -> Dict[str, str]:
def _analyze_reference_images(self, image_urls: list) -> Dict[str, Any]:
"""
포스터에 들어갈 텍스트 내용 생성
Args:
request: 포스터 생성 요청
Returns:
포스터 텍스트 구성 요소들
참조 이미지들 분석
"""
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_descriptions = []
for image_path in request.image_paths:
try:
description = self.ai_client.analyze_image(image_path)
image_descriptions.append(description)
except:
continue
# AI 프롬프트 생성
prompt = f"""
당신은 소상공인을 위한 포스터 카피라이터입니다.
다음 정보를 바탕으로 매력적인 포스터 문구를 작성해주세요.
**매장 정보:**
- 매장명: {request.store_name or '우리 가게'}
- 카테고리: {request.category}
- 추가 정보: {request.additional_info or '없음'}
**이벤트 정보:**
- 이벤트 제목: {request.event_title or '특별 이벤트'}
- 할인 정보: {request.discount_info or '특가 진행'}
- 시작 시간: {request.start_time or '상시'}
- 종료 시간: {request.end_time or '상시'}
**이미지 설명:**
{chr(10).join(image_descriptions) if image_descriptions else '이미지 없음'}
다음 형식으로 응답해주세요:
1. 메인 헤드라인 (10글자 이내, 임팩트 있게)
2. 서브 헤드라인 (20글자 이내, 구체적 혜택)
3. 설명 문구 (30글자 이내, 친근하고 매력적으로)
4. 행동 유도 문구 (15글자 이내, 액션 유도)
항목은 줄바꿈으로 구분해서 작성해주세요.
"""
# AI로 텍스트 생성
generated_text = self.ai_client.generate_text(prompt)
# 생성된 텍스트 파싱
lines = generated_text.strip().split('\n')
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 {
'main_headline': lines[0] if len(lines) > 0 else request.event_title or '특별 이벤트',
'sub_headline': lines[1] if len(lines) > 1 else request.discount_info or '지금 바로!',
'description': lines[2] if len(lines) > 2 else '특별한 혜택을 놓치지 마세요',
'call_to_action': lines[3] if len(lines) > 3 else '지금 방문하세요!'
'total_images': len(image_urls),
'results': analysis_results
}
def _process_images(self, image_paths: list) -> list:
"""
포스터에 사용할 이미지들 전처리
Args:
image_paths: 원본 이미지 경로 리스트
Returns:
전처리된 이미지 객체 리스트
"""
processed_images = []
for image_path in image_paths:
finally:
# 임시 파일 정리
for temp_file in temp_files:
try:
# 이미지 로드 및 리사이즈
image = Image.open(image_path)
# RGBA로 변환 (투명도 처리)
if image.mode != 'RGBA':
image = image.convert('RGBA')
# 포스터에 맞게 리사이즈 (최대 400x400)
image.thumbnail((400, 400), Image.Resampling.LANCZOS)
processed_images.append(image)
except Exception as e:
print(f"이미지 처리 오류 {image_path}: {e}")
continue
return processed_images
def _create_poster_image(self, request: PosterRequest, poster_text: Dict[str, str], images: list) -> Image.Image:
"""
실제 포스터 이미지 생성
Args:
request: 포스터 생성 요청
poster_text: 포스터 텍스트
images: 전처리된 이미지 리스트
Returns:
생성된 포스터 이미지
"""
# 카테고리별 테마 적용
theme = self.category_themes.get(request.category, self.category_themes['음식'])
# 캔버스 생성
poster = Image.new('RGBA',
(self.poster_config['width'], self.poster_config['height']),
theme['background'])
draw = ImageDraw.Draw(poster)
# 폰트 설정 (시스템 기본 폰트 사용)
try:
# 다양한 폰트 시도
title_font = ImageFont.truetype("arial.ttf", 60)
subtitle_font = ImageFont.truetype("arial.ttf", 40)
text_font = ImageFont.truetype("arial.ttf", 30)
small_font = ImageFont.truetype("arial.ttf", 24)
os.remove(temp_file)
except:
# 기본 폰트 사용
title_font = ImageFont.load_default()
subtitle_font = ImageFont.load_default()
text_font = ImageFont.load_default()
small_font = ImageFont.load_default()
# 레이아웃 계산
y_pos = 80
# 1. 메인 헤드라인
main_headline = poster_text['main_headline']
bbox = draw.textbbox((0, 0), main_headline, font=title_font)
text_width = bbox[2] - bbox[0]
x_pos = (self.poster_config['width'] - text_width) // 2
draw.text((x_pos, y_pos), main_headline,
fill=theme['primary'], font=title_font)
y_pos += 100
# 2. 서브 헤드라인
sub_headline = poster_text['sub_headline']
bbox = draw.textbbox((0, 0), sub_headline, font=subtitle_font)
text_width = bbox[2] - bbox[0]
x_pos = (self.poster_config['width'] - text_width) // 2
draw.text((x_pos, y_pos), sub_headline,
fill=theme['secondary'], font=subtitle_font)
y_pos += 80
# 3. 이미지 배치 (있는 경우)
if images:
image_y = y_pos + 30
if len(images) == 1:
# 단일 이미지: 중앙 배치
img = images[0]
img_x = (self.poster_config['width'] - img.width) // 2
poster.paste(img, (img_x, image_y), img)
y_pos = image_y + img.height + 50
elif len(images) == 2:
# 두 개 이미지: 나란히 배치
total_width = sum(img.width for img in images) + 20
start_x = (self.poster_config['width'] - total_width) // 2
for i, img in enumerate(images):
img_x = start_x + (i * (img.width + 20))
poster.paste(img, (img_x, image_y), img)
y_pos = image_y + max(img.height for img in images) + 50
else:
# 여러 이미지: 그리드 형태
cols = 2
rows = (len(images) + cols - 1) // cols
img_spacing = 20
for i, img in enumerate(images[:4]): # 최대 4개
row = i // cols
col = i % cols
img_x = (self.poster_config['width'] // cols) * col + \
(self.poster_config['width'] // cols - img.width) // 2
img_y = image_y + row * (200 + img_spacing)
poster.paste(img, (img_x, img_y), img)
y_pos = image_y + rows * (200 + img_spacing) + 30
# 4. 설명 문구
description = poster_text['description']
# 긴 텍스트는 줄바꿈 처리
words = description.split()
lines = []
current_line = []
for word in words:
test_line = ' '.join(current_line + [word])
bbox = draw.textbbox((0, 0), test_line, font=text_font)
if bbox[2] - bbox[0] < self.poster_config['width'] - 100:
current_line.append(word)
else:
if current_line:
lines.append(' '.join(current_line))
current_line = [word]
if current_line:
lines.append(' '.join(current_line))
for line in lines:
bbox = draw.textbbox((0, 0), line, font=text_font)
text_width = bbox[2] - bbox[0]
x_pos = (self.poster_config['width'] - text_width) // 2
draw.text((x_pos, y_pos), line, fill=(80, 80, 80), font=text_font)
y_pos += 40
y_pos += 30
# 5. 기간 정보 (있는 경우)
if request.start_time and request.end_time:
period_text = f"기간: {request.start_time} ~ {request.end_time}"
bbox = draw.textbbox((0, 0), period_text, font=small_font)
text_width = bbox[2] - bbox[0]
x_pos = (self.poster_config['width'] - text_width) // 2
draw.text((x_pos, y_pos), period_text, fill=(120, 120, 120), font=small_font)
y_pos += 50
# 6. 행동 유도 문구 (버튼 스타일)
cta_text = poster_text['call_to_action']
bbox = draw.textbbox((0, 0), cta_text, font=subtitle_font)
text_width = bbox[2] - bbox[0]
text_height = bbox[3] - bbox[1]
# 버튼 배경
button_width = text_width + 60
button_height = text_height + 30
button_x = (self.poster_config['width'] - button_width) // 2
button_y = self.poster_config['height'] - 150
draw.rounded_rectangle([button_x, button_y, button_x + button_width, button_y + button_height],
radius=25, fill=theme['primary'])
# 버튼 텍스트
text_x = button_x + (button_width - text_width) // 2
text_y = button_y + (button_height - text_height) // 2
draw.text((text_x, text_y), cta_text, fill=(255, 255, 255), font=subtitle_font)
# 7. 매장명 (하단)
if request.store_name:
store_text = request.store_name
bbox = draw.textbbox((0, 0), store_text, font=text_font)
text_width = bbox[2] - bbox[0]
x_pos = (self.poster_config['width'] - text_width) // 2
y_pos = self.poster_config['height'] - 50
draw.text((x_pos, y_pos), store_text, fill=(100, 100, 100), font=text_font)
return poster
pass
def _encode_image_to_base64(self, image: Image.Image) -> str:
def _create_poster_prompt(self, request: PosterContentGetRequest, image_analysis: Dict[str, Any]) -> str:
"""
PIL 이미지를 base64 문자열로 인코딩
Args:
image: PIL 이미지 객체
Returns:
base64 인코딩된 이미지 문자열
포스터 생성을 위한 AI 프롬프트 생성 (한글)
"""
import io
# RGB로 변환 (JPEG 저장을 위해)
if image.mode == 'RGBA':
# 흰색 배경과 합성
background = Image.new('RGB', image.size, (255, 255, 255))
background.paste(image, mask=image.split()[-1])
image = background
# 바이트 스트림으로 변환
img_buffer = io.BytesIO()
image.save(img_buffer, format='JPEG', quality=90)
img_buffer.seek(0)
# base64 인코딩
img_base64 = base64.b64encode(img_buffer.getvalue()).decode('utf-8')
return f"data:image/jpeg;base64,{img_base64}"
# 기본 스타일 설정
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'])
# 색상 정보
color_info = ""
if image_analysis.get('results'):
colors = image_analysis['results'][0].get('dominant_colors', [])
if colors:
color_info = f"참조 색상 팔레트: {colors[:3]}을 활용한 조화로운 색감"
prompt = f"""
한국의 음식점/카페를 위한 전문적인 홍보 포스터를 디자인해주세요.
**메인 콘텐츠:**
- 제목: "{request.title}"
- 카테고리: {request.category}
- 콘텐츠 타입: {request.contentType}
**디자인 스타일 요구사항:**
- 포토 스타일: {photo_style}
- 카테고리 스타일: {category_style}
- 톤앤매너: {tone_style}
- 감정 강도: {emotion_design}
**이벤트 정보:**
- 이벤트명: {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

@ -0,0 +1,218 @@
"""
SNS 콘텐츠 생성 서비스
"""
import os
from typing import Dict, Any
from datetime import datetime
from utils.ai_client import AIClient
from utils.image_processor import ImageProcessor
from models.request_models import SnsContentGetRequest
class SnsContentService:
def __init__(self):
"""서비스 초기화"""
self.ai_client = AIClient()
self.image_processor = ImageProcessor()
# 플랫폼별 콘텐츠 특성 정의
self.platform_specs = {
'인스타그램': {
'max_length': 2200,
'hashtag_count': 15,
'style': '감성적이고 시각적',
'format': '짧은 문장, 해시태그 활용'
},
'네이버 블로그': {
'max_length': 3000,
'hashtag_count': 10,
'style': '정보성과 친근함',
'format': '구조화된 내용, 상세 설명'
}
}
# 톤앤매너별 스타일
self.tone_styles = {
'친근한': '반말, 이모티콘 활용, 편안한 어조',
'정중한': '존댓말, 격식 있는 표현, 신뢰감 있는 어조',
'재미있는': '유머 섞인 표현, 트렌디한 말투, 참신한 비유',
'전문적인': '전문 용어 활용, 체계적 설명, 신뢰성 강조'
}
# 감정 강도별 표현
self.emotion_levels = {
'약함': '은은하고 차분한 표현',
'보통': '적당히 활기찬 표현',
'강함': '매우 열정적이고 강렬한 표현'
}
def generate_sns_content(self, request: SnsContentGetRequest) -> Dict[str, Any]:
"""
SNS 콘텐츠 생성 (HTML 형식 반환)
"""
try:
# 이미지 다운로드 및 분석
image_analysis = self._analyze_images_from_urls(request.images)
# AI 프롬프트 생성
prompt = self._create_sns_prompt(request, image_analysis)
# AI로 콘텐츠 생성
generated_content = self.ai_client.generate_text(prompt)
# HTML 형식으로 포맷팅
html_content = self._format_to_html(generated_content, request)
return {
'success': True,
'content': html_content
}
except Exception as e:
return {
'success': False,
'error': str(e)
}
def _analyze_images_from_urls(self, image_urls: list) -> Dict[str, Any]:
"""
URL에서 이미지를 다운로드하고 분석
"""
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_info = self.image_processor.get_image_info(temp_path)
image_description = self.ai_client.analyze_image(temp_path)
analysis_results.append({
'url': image_url,
'info': image_info,
'description': image_description
})
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
def _create_sns_prompt(self, request: SnsContentGetRequest, image_analysis: Dict[str, Any]) -> str:
"""
SNS 콘텐츠 생성을 위한 AI 프롬프트 생성
"""
platform_spec = self.platform_specs.get(request.platform, self.platform_specs['인스타그램'])
tone_style = self.tone_styles.get(request.toneAndManner, '친근한 어조')
emotion_level = self.emotion_levels.get(request.emotionIntensity, '적당한 강도')
# 이미지 설명 추출
image_descriptions = []
for result in image_analysis.get('results', []):
if 'description' in result:
image_descriptions.append(result['description'])
prompt = f"""
당신은 소상공인을 위한 SNS 마케팅 콘텐츠 전문가입니다.
다음 정보를 바탕으로 {request.platform} 적합한 게시글을 작성해주세요.
**게시물 정보:**
- 제목: {request.title}
- 카테고리: {request.category}
- 콘텐츠 타입: {request.contentType}
**스타일 요구사항:**
- 톤앤매너: {request.toneAndManner} ({tone_style})
- 감정 강도: {request.emotionIntensity} ({emotion_level})
- 특별 요구사항: {request.requirement or '없음'}
**이벤트 정보:**
- 이벤트명: {request.eventName or '없음'}
- 시작일: {request.startDate or '없음'}
- 종료일: {request.endDate or '없음'}
**이미지 분석 결과:**
{chr(10).join(image_descriptions) if image_descriptions else '이미지 없음'}
**플랫폼 특성:**
- 최대 길이: {platform_spec['max_length']}
- 스타일: {platform_spec['style']}
- 형식: {platform_spec['format']}
**요구사항:**
1. {request.platform} 특성에 맞는 톤앤매너 사용
2. {request.category} 카테고리에 적합한 내용 구성
3. 고객의 관심을 있는 매력적인 문구 사용
4. 이미지와 연관된 내용으로 작성
5. 지정된 톤앤매너와 감정 강도에 맞게 작성
본문과 해시태그를 모두 포함하여 완성된 게시글을 작성해주세요.
"""
return prompt
def _format_to_html(self, content: str, request: SnsContentGetRequest) -> str:
"""
생성된 콘텐츠를 HTML 형식으로 포맷팅
"""
# 줄바꿈을 <br> 태그로 변환
content = content.replace('\n', '<br>')
# 해시태그를 파란색으로 스타일링
import re
content = re.sub(r'(#[\w가-힣]+)', r'<span style="color: #1DA1F2; font-weight: bold;">\1</span>', content)
# 이모티콘은 그대로 유지
# 전체 HTML 구조
html_content = f"""
<div style="font-family: 'Noto Sans KR', Arial, sans-serif; line-height: 1.6; padding: 20px; max-width: 600px;">
<div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 15px; border-radius: 10px 10px 0 0; text-align: center;">
<h3 style="margin: 0; font-size: 18px;">{request.platform} 게시물</h3>
</div>
<div style="background: white; padding: 20px; border-radius: 0 0 10px 10px; border: 1px solid #e1e8ed;">
<div style="font-size: 16px; color: #333;">
{content}
</div>
{self._add_metadata_html(request)}
</div>
</div>
"""
return html_content
def _add_metadata_html(self, request: SnsContentGetRequest) -> str:
"""
메타데이터를 HTML에 추가
"""
metadata_html = '<div style="margin-top: 20px; padding-top: 15px; border-top: 1px solid #e1e8ed; font-size: 12px; color: #666;">'
if request.eventName:
metadata_html += f'<div><strong>이벤트:</strong> {request.eventName}</div>'
if request.startDate and request.endDate:
metadata_html += f'<div><strong>기간:</strong> {request.startDate} ~ {request.endDate}</div>'
metadata_html += f'<div><strong>카테고리:</strong> {request.category}</div>'
metadata_html += f'<div><strong>생성일:</strong> {datetime.now().strftime("%Y-%m-%d %H:%M")}</div>'
metadata_html += '</div>'
return metadata_html

View File

@ -4,36 +4,96 @@ Claude AI 및 OpenAI API 호출을 담당
"""
import os
import base64
from typing import Optional
import requests
from typing import Optional, List
import anthropic
import openai
from PIL import Image
import io
class AIClient:
"""AI API 클라이언트 클래스"""
def __init__(self):
"""AI 클라이언트 초기화"""
self.claude_api_key = os.getenv('CLAUDE_API_KEY')
self.openai_api_key = os.getenv('OPENAI_API_KEY')
# Claude 클라이언트 초기화
if self.claude_api_key:
self.claude_client = anthropic.Anthropic(api_key=self.claude_api_key)
else:
self.claude_client = None
# OpenAI 클라이언트 초기화
if self.openai_api_key:
openai.api_key = self.openai_api_key
self.openai_client = openai
self.openai_client = openai.OpenAI(api_key=self.openai_api_key)
else:
self.openai_client = None
def download_image_from_url(self, image_url: str) -> str:
"""
URL에서 이미지를 다운로드하여 임시 파일로 저장
Args:
image_url: 다운로드할 이미지 URL
Returns:
임시 저장된 파일 경로
"""
try:
response = requests.get(image_url, timeout=30)
response.raise_for_status()
# 임시 파일로 저장
import tempfile
import uuid
file_extension = image_url.split('.')[-1] if '.' in image_url else 'jpg'
temp_filename = f"temp_{uuid.uuid4()}.{file_extension}"
temp_path = os.path.join('uploads', 'temp', temp_filename)
# 디렉토리 생성
os.makedirs(os.path.dirname(temp_path), exist_ok=True)
with open(temp_path, 'wb') as f:
f.write(response.content)
return temp_path
except Exception as e:
print(f"이미지 다운로드 실패 {image_url}: {e}")
return None
def generate_image_with_openai(self, prompt: str, size: str = "1024x1024") -> str:
"""
OpenAI DALL-E를 사용하여 이미지 생성
Args:
prompt: 이미지 생성 프롬프트
size: 이미지 크기 (1024x1024, 1792x1024, 1024x1792)
Returns:
생성된 이미지 URL
"""
try:
if not self.openai_client:
raise Exception("OpenAI API 키가 설정되지 않았습니다.")
response = self.openai_client.images.generate(
model="dall-e-3",
prompt=prompt,
size=size,
quality="standard",
n=1,
)
return response.data[0].url
except Exception as e:
print(f"OpenAI 이미지 생성 실패: {e}")
raise Exception(f"이미지 생성 중 오류가 발생했습니다: {str(e)}")
def generate_text(self, prompt: str, max_tokens: int = 1000) -> str:
"""
텍스트 생성 (Claude 우선, 실패시 OpenAI 사용)
Args:
prompt: 생성할 텍스트의 프롬프트
max_tokens: 최대 토큰
Returns:
생성된 텍스트
"""
# Claude AI 시도
if self.claude_client:
@ -48,6 +108,7 @@ class AIClient:
return response.content[0].text
except Exception as e:
print(f"Claude AI 호출 실패: {e}")
# OpenAI 시도
if self.openai_client:
try:
@ -61,19 +122,18 @@ class AIClient:
return response.choices[0].message.content
except Exception as e:
print(f"OpenAI 호출 실패: {e}")
# 기본 응답 (AI 서비스 모두 실패시)
# 기본 응답
return self._generate_fallback_content(prompt)
def analyze_image(self, image_path: str) -> str:
"""
이미지 분석 설명 생성
Args:
image_path: 분석할 이미지 경로
Returns:
이미지 설명 텍스트
"""
try:
# 이미지를 base64로 인코딩
image_base64 = self._encode_image_to_base64(image_path)
# Claude Vision API 시도
if self.claude_client:
try:
@ -103,6 +163,7 @@ class AIClient:
return response.content[0].text
except Exception as e:
print(f"Claude 이미지 분석 실패: {e}")
# OpenAI Vision API 시도
if self.openai_client:
try:
@ -130,41 +191,31 @@ class AIClient:
return response.choices[0].message.content
except Exception as e:
print(f"OpenAI 이미지 분석 실패: {e}")
except Exception as e:
print(f"이미지 분석 전체 실패: {e}")
# 기본 설명 반환
return "맛있고 매력적인 음식점의 특별한 순간"
def _encode_image_to_base64(self, image_path: str) -> str:
"""
이미지 파일을 base64로 인코딩
Args:
image_path: 이미지 파일 경로
Returns:
base64 인코딩된 이미지 문자열
"""
"""이미지 파일을 base64로 인코딩"""
with open(image_path, "rb") as image_file:
# 이미지 크기 조정 (API 제한 고려)
image = Image.open(image_file)
# 최대 크기 제한 (1024x1024)
if image.width > 1024 or image.height > 1024:
image.thumbnail((1024, 1024), Image.Resampling.LANCZOS)
# JPEG로 변환하여 파일 크기 줄이기
if image.mode == 'RGBA':
background = Image.new('RGB', image.size, (255, 255, 255))
background.paste(image, mask=image.split()[-1])
image = background
img_buffer = io.BytesIO()
image.save(img_buffer, format='JPEG', quality=85)
img_buffer.seek(0)
return base64.b64encode(img_buffer.getvalue()).decode('utf-8')
def _generate_fallback_content(self, prompt: str) -> str:
"""
AI 서비스 실패시 기본 콘텐츠 생성
Args:
prompt: 원본 프롬프트
Returns:
기본 콘텐츠
"""
"""AI 서비스 실패시 기본 콘텐츠 생성"""
if "콘텐츠" in prompt or "게시글" in prompt:
return """안녕하세요! 오늘도 맛있는 하루 되세요 😊
우리 가게의 특별한 메뉴를 소개합니다!