AI 이미지 생성 관련 코드 통합.
def generate_prompt_with_gpt4o(user_input):
response = GPT_CLIENT.chat.completions.create(
model="gpt-4o",
messages=[
{
"role": "system",
"content": """You are an expert in converting user's natural language descriptions into DALL-E image generation prompts.
##Main Guidelines
1. Carefully analyze the user's description to identify key elements.
2. Use clear and specific language to write the prompt.
3. Include details such as the main subject, style, composition, color, and lighting.
4. Appropriately utilize artistic references or cultural elements.
5. Add instructions about image quality or resolution if necessary.
##Prompt Structure
- Specify the main subject first, then add details
- Use adjectives and adverbs for mood and style
- Specify composition or perspective if needed
##Format Example:
"[Style/mood] image of [main subject]. [Detailed description]. [Composition]. [Color/lighting]."
"""
},
{"role": "user", "content": user_input}
],
temperature=0.7
)
def save_image_to_blob(image_url, prompt):
"""이미지를 Azure Blob Storage에 저장"""
try:
response = requests.get(image_url, stream=True)
response.raise_for_status()
sanitized_filename = re.sub(r'[<>:"/\\|?*]', '', prompt[:30]).strip()
filename = f"{sanitized_filename}.png"
blob_service_client = BlobServiceClient.from_connection_string(settings.AZURE_CONNECTION_STRING)
blob_client = blob_service_client.get_blob_client(
container=settings.CONTAINER_NAME,
blob=filename
)
blob_client.upload_blob(response.content, overwrite=True)
return blob_client.url
except Exception as e:
logging.error(f"Blob Storage 저장 중 오류 발생: {str(e)}", exc_info=True)
return None
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s [%(levelname)s] %(message)s',
handlers=[
logging.FileHandler('ai_generation.log'),
logging.StreamHandler()
]
)
class AIGeneration(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
prompt = models.TextField(help_text="사용자가 입력한 원본 프롬프트")
generated_prompt = models.TextField(help_text="GPT가 생성한 프롬프트")
image_url = models.URLField(max_length=1000)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ['-created_at']
verbose_name = 'AI 생성 이미지'
verbose_name_plural = 'AI 생성 이미지들'
AI 이미지 생성 기능을 게시물 작성 폼에 통합하는 과정 진행.
from django import forms
from .models import Post
class PostWithAIForm(forms.ModelForm):
prompt = forms.CharField(
widget=forms.Textarea,
required=False,
help_text='AI 이미지 생성을 위한 프롬프트'
)
class Meta:
model = Post
fields = ["title", "content", "tag_set"]
class PostEditForm(forms.ModelForm):
class Meta:
model = Post
fields = ["title", "content", "tag_set"] # image field 제외
@login_required
def create_post(request: HttpRequest) -> HttpResponse:
"""게시물 생성 (AI 이미지 생성 통합)"""
if request.method == "POST":
form = PostWithAIForm(request.POST)
if form.is_valid():
post = form.save(commit=False)
post.user = request.user
# AI로 생성된 이미지 URL이 있다면 저장
generated_image_url = request.POST.get('generated_image_url')
if generated_image_url:
post.image = generated_image_url
post.save()
form.save_m2m()
return redirect("index")
else:
form = PostWithAIForm()
return render(request, "app/create_post.html", {"form": form})
@login_required
def generate_image(request) -> JsonResponse:
"""AI 이미지 생성 API"""
if request.method != "POST":
return JsonResponse({"error": "POST method required"}, status=405)
prompt = request.POST.get("prompt", "").strip()
if not prompt:
return JsonResponse({"error": "프롬프트를 입력해주세요."}, status=400)
try:
# GPT로 프롬프트 생성
generated_prompt = generate_prompt_with_gpt4o(prompt)
if not generated_prompt:
return JsonResponse({"error": "프롬프트 생성에 실패했습니다."}, status=500)
# DALL-E로 이미지 생성
image_url = generate_image_with_dalle(generated_prompt)
if not image_url:
return JsonResponse({"error": "이미지 생성에 실패했습니다."}, status=500)
# Blob Storage에 이미지 저장
blob_url = save_image_to_blob(image_url, generated_prompt)
if not blob_url:
return JsonResponse({"error": "이미지 저장에 실패했습니다."}, status=500)
# DB에 기록 저장
AIGeneration.objects.create(
user=request.user,
prompt=prompt,
generated_prompt=generated_prompt,
image_url=blob_url
)
return JsonResponse({
"image_url": blob_url,
"generated_prompt": generated_prompt
})
except Exception as e:
logging.error(f"이미지 생성 중 오류 발생: {str(e)}", exc_info=True)
return JsonResponse({"error": str(e)}, status=500)
{% extends "app/base.html" %}
{% load django_bootstrap5 %}
{% block content %}
<form method="post" id="postForm">
{% csrf_token %}
{% bootstrap_form form %}
<div id="imagePreview" style="display: none;">
<img id="generatedImage" src="" alt="">
<input type="hidden" name="generated_image_url" id="generatedImageUrl">
</div>
<button type="button" onclick="generateImage()">이미지 생성</button>
<button type="submit">저장</button>
</form>
<script>
async function generateImage() {
const promptInput = document.querySelector('[name="prompt"]');
const csrfToken = document.querySelector('[name="csrfmiddlewaretoken"]').value;
if (!promptInput || !promptInput.value.trim()) {
alert('프롬프트를 입력해주세요.');
return;
}
try {
const response = await fetch('/app/ai/generate/', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-CSRFToken': csrfToken
},
body: `prompt=${encodeURIComponent(promptInput.value)}`
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || '이미지 생성에 실패했습니다.');
}
const data = await response.json();
document.getElementById('generatedImage').src = data.image_url;
document.getElementById('generatedImageUrl').value = data.image_url;
document.getElementById('imagePreview').style.display = 'block';
} catch (error) {
alert(error.message);
console.error('Error:', error);
}
}
</script>
{% endblock %}
from django.urls import path
from . import views
urlpatterns = [
path("", views.index, name="index"),
path("posts/new/", views.create_post, name="create_post"),
path("posts/<int:pk>/", views.post_detail, name="post_detail"),
path("posts/<int:pk>/edit/", views.edit_post, name="edit_post"),
path("posts/<int:pk>/delete/", views.delete_post, name="delete_post"),
path("ai/generate/", views.generate_image, name="generate_image")
]
Azure Blob Storage에 DALL-E 생성 이미지 파일 업로드 시 파일명이 중복되어 기존 파일이 덮어씌워지는 문제가 발생했다. 여러 사용자가 비슷한 프롬프트로 이미지를 생성하려 하면 특히 문제가 될 수 있겠다 싶었다.
파일명을 고유하게 생성하기 위해 아래 요소들을 조합:
import uuid
from datetime import datetime
def save_image_to_blob(image_url, prompt, user_id):
"""이미지를 Azure Blob Storage에 저장"""
try:
response = requests.get(image_url, stream=True)
response.raise_for_status()
# 현재 시간의 타임스탬프와 UUID를 조합하여 고유한 파일명 생성
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
unique_id = str(uuid.uuid4())[:8] # UUID의 첫 8자리만 사용
# 프롬프트에서 파일명으로 사용할 수 없는 문자 제거
sanitised_prompt = re.sub(r'[<>:"/\\|?*]', '', prompt[:20]).strip()
# 파일명 형식: user_id_timestamp_uuid_prompt.png
filename = f"user_{user_id}_{timestamp}_{unique_id}_{sanitised_prompt}.png"
blob_service_client = BlobServiceClient.from_connection_string(settings.AZURE_CONNECTION_STRING)
blob_client = blob_service_client.get_blob_client(
container=settings.CONTAINER_NAME,
blob=filename
)
blob_client.upload_blob(response.content, overwrite=True)
return blob_client.url
except Exception as e:
logging.error(f"Blob Storage 저장 중 오류 발생: {str(e)}", exc_info=True)
return None
게시글을 삭제할 때 스토리지에는 이미지가 계속 남아있는 문제 발견.
불필요한 이미지 파일이 storage에 남는 것을 방지할 수 있도록 Blob Storage에 저장된 이미지도 함께 삭제하는 기능을 추가하였다.
from django.db import models
from django.conf import settings
from django.contrib.auth.models import User
from azure.storage.blob import BlobServiceClient
import logging
from urllib.parse import urlparse
class Post(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
title = models.CharField(max_length=100)
content = models.TextField()
image = models.URLField(blank=True, null=True)
# ... 기타 필드 ...
def delete(self, *args, **kwargs):
# 이미지가 있는 경우에만 삭제 시도
if self.image:
try:
# Blob 서비스 클라이언트 생성
blob_service_client = BlobServiceClient.from_connection_string(
settings.AZURE_CONNECTION_STRING
)
container_client = blob_service_client.get_container_client(
settings.CONTAINER_NAME
)
# URL에서 blob 이름 추출
blob_name = urlparse(self.image).path.split('/')[-1]
# Blob 삭제
container_client.delete_blob(blob_name)
logging.info(f"Blob {blob_name} deleted successfully")
except Exception as e:
logging.error(f"Error deleting blob: {str(e)}")
# 상위 클래스의 delete 메서드 호출
super().delete(*args, **kwargs)
<div id="imagePreview" style="display: none;">
<img id="generatedImage" src="" alt="">
<input type="hidden" name="generated_image_url" id="generatedImageUrl">
<div class="mt-2">
<button type="button" class="btn btn-secondary" onclick="cancelImage()">취소</button>
</div>
</div>
<script>
function cancelImage() {
const imagePreview = document.getElementById('imagePreview');
const generatedImage = document.getElementById('generatedImage');
const generatedImageUrl = document.getElementById('generatedImageUrl');
generatedImage.src = '';
generatedImageUrl.value = '';
imagePreview.style.display = 'none';
}
</script>
@login_required
def generate_image(request):
"""이미지 생성 뷰"""
if request.method != "POST":
return JsonResponse({"error": "POST method required"}, status=405)
prompt = request.POST.get("prompt", "").strip()
if not prompt:
return JsonResponse({"error": "프롬프트를 입력해주세요."}, status=400)
try:
generated_prompt = generate_prompt_with_gpt4o(prompt)
if not generated_prompt:
return JsonResponse({"error": "프롬프트 생성에 실패했습니다."}, status=500)
image_url = generate_image_with_dalle(generated_prompt)
if not image_url:
return JsonResponse({"error": "이미지 생성에 실패했습니다."}, status=500)
# DALL-E URL 직접 반환 (Blob Storage 저장 로직 제거)
return JsonResponse({
"image_url": image_url,
"generated_prompt": generated_prompt
})
except Exception as e:
logging.error(f"이미지 생성 중 오류 발생: {str(e)}", exc_info=True)
return JsonResponse({"error": str(e)}, status=500)
@login_required
def create_post(request: HttpRequest) -> HttpResponse:
if request.method == "POST":
form = PostWithAIForm(request.POST)
if form.is_valid():
post = form.save(commit=False)
post.user = request.user
generated_image_url = request.POST.get('generated_image_url')
if generated_image_url:
blob_url = save_image_to_blob(generated_image_url, form.cleaned_data['prompt'], request.user.id)
if blob_url:
post.image = blob_url
AIGeneration.objects.create(
user=request.user,
prompt=form.cleaned_data['prompt'],
generated_prompt="",
image_url=blob_url
)
post.save()
form.save_m2m()
return redirect("index")
class Post(models.Model):
# ... 다른 필드들 ...
image = models.URLField(blank=True, null=True, max_length=1000) # max_length 증가