[Spring boot] Kakao Karlo Open API와 Chat GPT API로 AI 이미지 생성 웹사이트 만들기 - 2

dev asdf·2024년 3월 22일
0

Spring Boot

목록 보기
4/7

🔔 지난 글

https://velog.io/@asdf-dev/spring-boot-rest-template


이제 본격적으로 Open api를 통해 AI 이미지를 생성하는 웹사이트를 만들어보자.

GPT도 이미지 생성 api를 제공하므로, 꼭 Karlo를 사용할 필요는 없다.

하지만 Karlo는 공짜임

우선 Karlo 부터 연동해보자.

📌 Karlo 문서


❗❗❗ 24.09.30 - Karlo API 서비스 종료

https://devtalk.kakao.com/t/kogpt-karlo-api-notice-end-of-support-for-kogpt-karlo-apis/139024

Karlo API가 9월 30일자부로 서비스가 종료되었다.... 😯 ㅋㅋㅋ

따라서 기존의 Karlo api를 연동하는 부분을 GPT api를 사용하는 것으로 대체하기로 한다.

Chat GPT API로 AI 이미지 생성 웹사이트 만들기가 됐네...



[Deprecated] Karlo 연동하기


1. Model

이미지를 받을 DTO를 추가한다.

ImageDto.java

//  [24/09/30 - deprecated] karlo 이미지 생성 api
//  import lombok.AllArgsConstructor;
//  import lombok.Getter;
//  import lombok.NoArgsConstructor;
//  import lombok.Setter;
//
//  @Getter
//  @Setter
//  @NoArgsConstructor
//  @AllArgsConstructor
//  public class ImageDto {
//      private String id;
//      private String image;
//  }

위의 예제 응답처럼 id와 image를 추출할 생각이다.


2. Service

Karlo api를 연동한다.

RestApiService.java

import com.communitcation.rest.client.CommunicationInfo;
import com.communitcation.rest.model.ImageDto;
import com.communitcation.rest.model.UrlDto;
import com.google.gson.reflect.TypeToken;
import lombok.RequiredArgsConstructor;
import org.apache.hc.core5.http.Method;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.net.URLDecoder;
import java.util.List;
import java.util.Map;

import static com.communitcation.rest.util.JsonUtil.fromJson;
import static com.communitcation.rest.util.JsonUtil.toJson;

@Service
@RequiredArgsConstructor
public class RestApiService {

    private final CommunicationService communicationService;

    @Value("${api.naver.client-id}")
    private String naverClientId;

    @Value("${api.naver.client-key}")
    private String naverClientKey;

   // @Value("${api.kakao.key}")
   // private String kakaoRestApiKey; // 카카오 api key 추가

   ... 생략 ...

	
    // [24/09/30 - deprecated] karlo 이미지 생성 api
    //    public String makeAiImageByKakao(String prompt) throws IOException {
    //        Map<String,String> karloReqHeader = Map.of(
    //                "Authorization", "KakaoAK " + kakaoRestApiKey
    //        );
    //
    //        String host = "api.kakaobrain.com";
    //        String genImgPath = "/v2/inference/karlo/t2i";
    //        prompt = prompt.replaceAll("prompt=","");
    //        String koToEng = translateKoToEng(prompt);
    //
    //        Map<String, Object> requestMap = Map.of(
    //                "version" , "v2.1",
    //                "prompt",koToEng,
    //                "negative_prompt","low quality, low contrast, draft, amateur, cut off, cropped, frame",
    //                "width" , 768,
    //                "height", 768,
    //                "image_format","png",
    //                "image_quality",100
    //        );
    //
    //        CommunicationInfo karloGenApiInfo = CommunicationInfo
    //                .builder()
    //                .scheme("https")
    //                .port("443")
    //                .host(host)
    //                .path(genImgPath)
    //                .method(Method.POST)
    //                .headers(karloReqHeader)
    //                .requestData(requestMap)
    //                .responseClazz(Map.class)
    //                .build();
    //        Map<String,Object> imgResMap = communicationService.communicate(karloGenApiInfo);
    //        System.out.println(imgResMap);
    //        String imagesToStr = toJson(imgResMap.get("images"));
    //        TypeToken<List<ImageDto>> imagesTypeToken = new TypeToken<List<ImageDto>>() {};
    //        List<ImageDto> images = fromJson(imagesToStr, imagesTypeToken);
    //        ImageDto imageDto = images.get(0);
    //        String imageUrl =  imageDto.getImage();
    //        return imageUrl;
    //    }

}

최종적으로 이미지의 url을 반환하게끔 했다.

request는 다음처럼 설정했다.

// Map<String, Object> requestMap = Map.of(
//     "version" , "v2.1",
//     "prompt",prompt,
//     "negative_prompt","low quality, low contrast, draft, amateur, cut off, cropped, frame",
//     "width" , 768,
//     "height", 768,
//     "image_format","png",
//     "image_quality",100
// );

버전은 2.1, negative_prompt는 예시로 적어둔거 그대로 복사해서 씀ㅋㅋㅋ

width랑 height는 최소로 설정해놨다.

이외에도 노이즈 조정 등 다양한 파라미터가 있다. 난 기본적인거만 세팅함.

참고로, Karlo는 영어 프롬프트만 지원한다.

그치만 내가 한글 프롬프트를 쓰고 싶다면 어떡할거지?

지난글에서도 언급했다시피, 무려 5$를 투자한 GPT api를 번역기로 쓰면된다.

그럼 이놈이 내가 쓴 한글 프롬프트를 영어로 바꿔서 Karlo에게 넘겨줄 것이다.


3. Controller

🔨 [24.09.30] HomeController.java

// import com.communitcation.rest.service.RestApiService;
import com.communitcation.rest.service.GenerateImageService;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
// import java.io.IOException;

@Controller
@RequiredArgsConstructor
public class HomeController {

	//  [24/09/30 - deprecated] karlo 이미지 생성 api
    // private final RestApiService restApiService;

    //    @PostMapping("/karlo-img")
    //    public ResponseEntity<String> kakaoGenAiImageApi(
    //            @RequestBody  String prompt,
    //            HttpServletRequest request
    //    ) throws IOException {
    //          String genImage = restApiService.makeAiImageByKakao(prompt);
    //          return ResponseEntity.ok(genImage);
    //    }
    
    private final GenerateImageService generateImageService;
    
    @PostMapping("/ai-img")
    public String openAiImageApi(
            @RequestBody  String prompt,
            HttpServletRequest request
    ) {
          String genImage =  generateImageService.generateImageUrl(prompt);
          return ResponseEntity.ok(genImage);
    }
    
}

kakaoGenAiImageApi 메서드를 위와 같이 수정한다. GPT api 호출을 위해, GenerateImageService 라는 별도의 서비스 로직을 작성할 것이다.


GPT 연동하기


Java는 Azure에서 지원하는 라이브러리랑 Theo Kanning 이라는 개발자가 만든 다음 라이브러리가 주로 사용되는 듯 하다. 둘 다 공식문서에 있음
난 👇 이거 쓸거다.

https://github.com/TheoKanning/openai-java


❗❗❗ 24.06.07 - This repository has been archived by the owner on Jun 7, 2024. It is now read-only.

해당 라이브러리도 6월 이후로 업데이트를 지원하지 않는다고 한다. ㅋㅋㅋㅋ

그래도 /v1/chat/completions, /v1/images/generations api를 호출하는데는 무리가 없어보이므로 그대로 사용하겠다.


빠르게 시작하고 싶으면 OpenAiService를 쓰라고 한다.

사실 지난글에서 이미 이 라이브러리를 build.gradle에 추가해놨기 때문에 별도로 gradle 추가하는 부분은 생략하였다.

4. Service

GptService.java

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.theokanning.openai.service.OpenAiService;

@Configuration
public class GptService {

    @Value("${api.gpt.key}")
    private String gptKey;
    
    @Bean
    public OpenAiService openAiService(){
          OpenAiService service = new OpenAiService(gptKey, Duration.ofSeconds(120));
          return service;
    }
}

Bean으로 OpenAiService를 등록해준다.
지금 생각해보니 Service 폴더보단 Config 폴더가 어울리는 거 같기도..
귀찮으니 그냥 이대로 냅두겠다.

🔨 [24.09.30] RestApiService.java -> GenerateImageService.java

import com.theokanning.openai.completion.chat.*;
import com.theokanning.openai.image.CreateImageRequest;
import com.theokanning.openai.image.ImageResult;
import com.theokanning.openai.service.OpenAiService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.net.URLDecoder;
import java.util.List;

@Service
public class GenerateImageService {

    @Autowired
    private GptService gptService;
    private OpenAiService openAiService;

    public GenerateImageService(GptService gptService){
        this.gptService = gptService;
        this.openAiService = gptService.openAiService();
    }

	// 한 -> 영 번역
    private String translateKoToEng(String prompt){
        String decodePrompt = URLDecoder.decode(prompt);
        String message = """
                Translate the following Korean text to English and Return only the translated result in English.: %s """.formatted(decodePrompt);
        ChatMessage chatMessage = new ChatMessage(ChatMessageRole.USER.value(),message);
        ChatCompletionRequest chatCompletionRequest = ChatCompletionRequest.builder()
                .messages(List.of(chatMessage))
                .model("gpt-4o-mini")
                .build();

        ChatCompletionResult response = openAiService.createChatCompletion(chatCompletionRequest);
        ChatCompletionChoice choice = response.getChoices().get(0);
        String translatePrompt = choice.getMessage().getContent();
        String log  = """
                [%s]\ntranslate: %s""".formatted(decodePrompt,translatePrompt);
        System.out.println(log);
        return translatePrompt;
    }

	// ai 이미지 생성
    public String generateImageUrl(String prompt){
        String message = translateKoToEng(prompt);
        CreateImageRequest createImageRequest = CreateImageRequest.builder()
                .prompt(message)
                .build();
        ImageResult images = openAiService.createImage(createImageRequest);
        String imageUrl = images.getData().get(0).getUrl();
        return imageUrl;
    }

}

공식 문서에서는 Completions는 레거시라면서 ChatCompletions을 사용할 것을 권장함

Completions API Legacy
The completions API endpoint received its final update in July 2023 and has a different interface than the new chat completions endpoint. Instead of the input being a list of messages, the input is a freeform text string called a prompt.

다음처럼 프롬프트를 입력하면 번역을 해준다고 한다.
그대로 복사해서 message에 설정해주었다.

사실 "<- translate to eng" 이렇게 썼었는데 등신같이 번역되길래 공식 문서를 봤다...😲


❗❗❗ 24.10.08 - 지원 종료된 모델

OpenAiHttpException: The model gpt-4-vision-preview has been deprecated, learn more here: https://platform.openai.com/docs/deprecations] with root cause

gpt-4-vision-preview 가 deprecated 되었다.
대신, gpt-3.5-turbo 보다 더 저렴하고 성능이 좋다고 하는 gpt-4o-mini 를 사용하면 된다.


GPT 이미지 생성 api도 한글 프롬프트를 전달하면 엉뚱한 이미지가 생성되길래, translateKoToEng 메서드를 GenerateImageService 내부로 옮겨서 재사용했다.

뭔데 이거... 이렇게 페퍼로니 피자의 흔적이라곤 전혀 찾아볼 수 없는 결과가 나온다.

한영번역을 적용하면 피자가 잘 만들어진다.

맛있겠다.


백엔드 작업은 끝났다.

Thymeleaf로 웹사이트의 구색을 갖춰주자.

application.yml

spring:
  devtools:
    livereload:
      enabled: true

  thymeleaf:
    prefix: classpath:/templates/
    suffix: .html
    cache: false
    check-template-location: true

... 생략 ...

yml 파일 상단에 타임리프 설정을 추가해준다.

5. Resources > Templates

main 하위 폴더에 있는 resource > templates 폴더에 index.html을 만들어주자.

🔨 [24.09.30] index.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>HELLO AI</title>
    <style>
        body {
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: flex-start;
            height: 100vh;
            margin: 0;
            padding-top: 1.5vh;
        }
        form {
            margin-bottom: 10px;
            padding: 20px;
            width: 600px;
            display: flex;
            align-items: center;
        }
        input[type="text"] {
            flex: 1;
            padding: 10px;
            margin-right: 10px;
        }
        button {
            padding: 10px 20px;
        }
       </style>
</head>
<body>
    <h1>AI 이미지 생성기</h1>
    <!-- <form th:action="@{/karlo-img}" method="post"> --> 
    <form th:action="@{/ai-img}" method="post">
        <input type="text" id="prompt" name="prompt" placeholder="예: 산책하는 귀여운 웰시코기, 사진같은, 8k 이미지"
               required>
        <button type="submit">생성</button>
    </form>

    <div th:if="${session.genImage != null}">
        <img th:src="@{${session.genImage}}" alt="AI 이미지">
    </div>
</body>
</html>

참고로 이 html은 Chat GPT를 훈육하면서 얻어냈다. css 하기 시렁

index.html 원툴이므로 redirect 시 사진이 날아가는 것을 방지하기 위해 HttpSession을 사용했다.


form 태그의 /karlo-img/ai-img 로 수정한다.


6. Controller

HomeController에 index.html을 연결해주자.

🔨 [24.09.30] HomeController.java

import com.communitcation.rest.service.GenerateImageService;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;

@Controller
@RequiredArgsConstructor
public class HomeController {

    private final GenerateImageService generateImageService;

    @RequestMapping("/")
    public String index() {
        return "index";
    }

//  [24/09/30 - deprecated] karlo 이미지 생성 api
//    @PostMapping("/karlo-img")
//    public String kakaoGenAiImageApi(
//            @RequestBody  String prompt,
//            HttpServletRequest request
//    ) throws IOException {
//          String genImage =  restApiService.makeAiImageByKakao(prompt);
//          HttpSession session = request.getSession(); 
//          session.setAttribute("genImage",genImage); // 이미지 url 설정
//          session.setAttribute("prompt", prompt);
//        return "redirect:/";
//    }

    @PostMapping("/ai-img")
    public String openAiImageApi(
            @RequestBody  String prompt,
            HttpServletRequest request
    ) {
          String genImage =  generateImageService.generateImageUrl(prompt);
          HttpSession session = request.getSession();
          session.setAttribute("genImage",genImage);
          session.setAttribute("prompt", prompt);
        return "redirect:/";
    }

}

결과

서버 재부팅 후, localhost:8080 으로 접속한다.


예제를 그대로 입력해서 어떤 이미지가 생성되는지 확인해보겠다.

📷 귀여운 웰시코기

끝. 재밌었다ㅎㅎ

3개의 댓글

comment-user-thumbnail
2024년 4월 22일

좋은 글이네요 깃소스있나요

2개의 답글