AIㆍML / 개발자 / 퍼스널 컴퓨팅

스테이블 디퓨전으로 AI 이미지 생성하는 리액티브 자바스크립트 만들기

Matthew Tyson | InfoWorld 2023.07.10
모두가 아는 것처럼 생성형 AI가 최근 엄청난 인기를 끌고 있다. 오픈AI의 챗GPT, 구글 바드와 같은 텍스트 생성기가 대표적이다. 이외에도 텍스트 투 이미지(text-to-image) 생성기가 있는데, 이 분야에서 여기서 가장 앞서 나가는 것이 오픈소스 이미지 생성 AI 시스템인 스테이블 디퓨전(Stable Diffusion)이다. 여기서는 스테이블 디퓨전의 무료 평가판 API를 사용해 서비스에 연결해 작동하는 React.js 클라이언트를 만들어 보자.
 
ⓒ Getty Image Bank
 

스테이블 디퓨전 시작하기 

스테이블 디퓨전은 깃허브 리포지토리에서 받을 수 있다. 호스팅되는 스테이블 디퓨전 설치본과 상호작용하기 위한 엔드포인트를 제공하는 여러 프로젝트가 있으므로 직접 모델을 설정하고 학습시키지 않아도 된다. 가장 좋은 스테이블 디퓨전 인터페이스 중 하나는 무료 평가판을 제공하는 스테이블 디퓨전 API다. 이 API를 사용해 무료 오픈소스 프론트 엔드 자바스크립트 라이브러리인 리액트로 스테이블 디퓨전과 상호작용할 수 있다.

먼저 create-react-app 명령줄 툴을 사용해 새 리액트 애플리케이션을 시작한다. 이를 위해서는 노드(Node)와 NPM을 미리 설치해야 한다. 그런 다음 $ npm i -g create-react-app으로 create-react-app을 설치한다. 이제 $ create-react-app react-sd 명령으로 간단한 애플리케이션을 만들 수 있다. 테스트하려면 react-sd 디렉터리로 이동해 $ npm start를 입력한 다음 시작 페이지인 localhost:3000을 방문한다. 
 

리액트 UI 빌드 

리액트 및 스테이블 디퓨전을 설정했으니 이제 텍스트 프롬프트를 입력해 스테이블 디퓨전 API 엔드포인트로 보내고 결과 이미지를 표시하는 UI를 만들 차례다. 스크롤 가능한 간단한 창을 사용해 생성된 이미지 목록을 표시하는 방식으로 작동한다.

먼저, <예시 1>과 같이 프롬프트를 입력 받는 텍스트 상자와 여기 붙일 제출(Submit) 버튼을 만든다. 이 코드는 리액트의 기본 App.js 파일을 대체한다. 

<예시 1> 기본 App.js 파일에 텍스트 상자 및 제출 옵션 추가 
import React, { useState } from "react";

const App = () => {
  const [textPrompt, setTextPrompt] = useState("");
  const [prompts, setPrompts] = useState([]);

  const generateImage = async () => {
  };

  const handleClick = () => {
    setPrompts([...prompts, textPrompt]);
    setTextPrompt("");
    generateImage();
  };

  return (
    <div>
      <input
        type="text"
        placeholder="Enter a text prompt"
        onChange={(e) => setTextPrompt(e.target.value)}
      />
      <button onClick={handleClick}>Generate Image</button>
      <ul>
        {prompts.map((prompt) => (
          <li key={prompt}>{prompt}</li>
        ))}
      </ul>
    </div>
  );
};

export default App;

이제 애플리케이션은 텍스트 입력을 받아 이를 UI에 정렬되지 않은 목록으로 표시되는 배열에 저장한다. 프롬프트 입력과 제출된 프롬프트의 배열은 모두 useState 후크 변수다. 
 

API 엔드포인트에 연결 

여기까지 generateImage() 함수는 아무 일도 하지 않는다. 작업을 본격적으로 구현해 보자. 첫 번째 단계는 스테이블 디퓨전 엔드포인트에 텍스트 프롬프트를 제출하는 것이다. 스테이블 디퓨전 API는 여기에 설명된 대로 JSON 요청과 응답을 기다리는 RESTful 스타일이다. 이 문서에는 여기에 설명된 텍스트-투-이미지 엔드포인트를 포함한 API 엔드포인트 목록이 포함된다. 

한 가지 유의할 점은 스테이블 디퓨전 API가 브라우저 요청을 좋아하지 않는다는 것이다. 이는 보안과 관련된 사항이므로(API 키를 브라우저에 노출), 이 예제에서는 공개 프록시를 사용해 우회한다. 애플리케이션이 노출되는 실제 애플리케이션에서는 이 방법을 사용하면 안 된다. 실제 환경에서는 백엔드 애플리케이션을 사용해 키를 보관하고 프론트 엔드 요청에 집어넣으면 된다. 여기서는 편의를 위해 헤로쿠(Heroku)의 CORS 프록시를 사용한다.

전체적으로는 사용자의 텍스트를 받아서 이를 API 엔드포인트에 POST 요청으로 제출하는 것이다. JSON 본문에 API 키와 사용자 텍스트를 위한 필드가 있다. 응답은 JSON 응답 본문으로 돌아온다(형식에 대한 자세한 내용은 여기를 참조). 여기서 우리가 보는 핵심은 단일 요소, 즉 생성된 이미지의 URL이 포함되는 출력 배열이다. 

사용자 인터페이스에서 이 URL을 가져와 순서 없는 목록으로 된 텍스트 프롬프트 옆의 이미지 구성요소에 넣는다. <예시 2>는 <예시 1>을 발전시켜 이러한 작업을 수행하는 코드를 보여준다. 

<예시 2> API 호출 및 생성된 이미지 표시 
import React, { useState } from "react";

const App = () => {
  const [textPrompt, setTextPrompt] = useState("");
  const [prompts, setPrompts] = useState([]);

  const generateImage = async () => {
    const apiKey = "YOUR KEY HERE"; 
    const url = "https://stablediffusionapi.com/api/v3/text2img"; 
    const proxyUrl = "https://cors-anywhere.herokuapp.com/";

    const requestOptions = {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ key: apiKey, prompt: textPrompt }),
    };

    try {
      const response = await fetch(proxyUrl + url, requestOptions);
      const data = await response.json();
      
      // Get the image URL from the response
      const imageUrl = data.output[0];
      
      // Update the prompts array with the generated image URL
      setPrompts([...prompts, { prompt: textPrompt, imageUrl }]);
    } catch (error) {
      console.error("Error generating image:", error);
    }
  };
  
  const handleClick = async () => {
    setPrompts([...prompts, { prompt: textPrompt, imageUrl: "" }]);
    setTextPrompt("");
    await generateImage();
  };

  return (
    <div className="container">
      <input className="input-container"
        type="text"
        placeholder="Enter a text prompt"
        value={textPrompt}
        onChange={(e) => setTextPrompt(e.target.value)}
      />
      <button onClick={handleClick}>Generate Image</button>
      <ul className="prompts-list">
        {prompts.map((item, index) => (
          <li key={index} className="prompt-item">
            <p className="prompt-text">{item.prompt}</p>
            {item.imageUrl && <img onerror="removeImage($(this));" src={item.imageUrl} alt="Generated Image" className="generated-image"/>}
          </li>
        ))}
      </ul>
    </div>
  );
};

export default App;

여기서 두 가지가 중요하다. 첫째, “YOUR API KEY HERE” 부분에 실제 API 키를 입력해야 한다. 둘째, (앞서도 말했지만) 공개 인터넷에 접한 웹 애플리케이션에서는 키가 노출되므로 이렇게 하면 안 된다. 다음으로, 생성된 이미지를 추적하고 표시할 수 있도록 프롬프트의 useState 변수에 imageUrl 필드를 추가한 것을 볼 수 있다. 또한 생성된 이미지가 표시되도록 handleClick() 함수에서 generateImage()가 완료될 때까지 기다리기 위한 await도 있다.

npm start로 이 애플리케이션을 시작하고 브라우저에서 보면, 프롬프트를 처음 제출할 때 헤로쿠 CORS 프록시가 권한을 요청할 수 있다. 자바스크립트 콘솔에서 누락된 권한(상태 401)에 대한 메시지와 링크가 표시된다. 링크를 클릭해서 페이지가 열리면 임시 액세스 버튼을 클릭한다(<그림 1> 참조). 
 
<그림 1> CORS 프록시 사용 요청 ⓒ IDG

이제 프롬프트를 제출하면 애플리케이션이 <그림 2>와 같이 프롬프트와 이미지 목록을 표시한다.  
 
<그림 2> UI가 잘 작동한다. ⓒ IDG
 

애플리케이션 미세 조정

여기까지만 해도 모두 잘 작동하지만 몇 가지 개선할 수 있는 부분이 있다. 우선 모든 이미지에 대한 로딩 상태 표시가 있다. 또 다른 부분은 스테이블 디퓨전은 이미지가 언제 완료되어야 하는지에 대한 ETA 필드와 함께 처리 상태를 반환하는 경우가 종종 있다는 점이다. 더 정교한 UI로 처리 상태를 표시할 수 있다.

첫 번째 우선 순위는 스타일 개선이다. CSS를 추가해 이미지를 더 조밀한 형식으로 표시하고 텍스트에 스타일을 더해 보기 좋게 해보자. 

<예시 3> 스타일을 개선한 애플리케이션 
.container {
  text-align: center;
  margin-top: 50px;
}

.input-container {
  margin-bottom: 20px;
}

input[type="text"] {
  padding: 10px;
  font-size: 16px;
  border: none;
  border-radius: 4px;
}

button {
  padding: 10px 20px;
  font-size: 16px;
  background-color: #007bff;
  color: #fff;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

.prompts-list {
  list-style-type: none;
  padding: 0;
  margin: 0;
}

.prompt-item {
  display: flex;
  align-items: center;
  margin-bottom: 10px;
}

.prompt-text {
  flex: 1;
  font-size: 18px;
}

.generated-image {
  width: 200px;
  height: 200px;
  object-fit: cover;
  border-radius: 4px;
}

.loading-text {
  font-size: 14px;
  font-style: italic;
  color: #aaa;
}

이제 이 라인을 App.js 파일의 앞부분에 추가해 새 스타일을 가져올 수 있다. 
 
import "./App.css";

이제 <그림 3>과 같이 입력 텍스트, 버튼 프롬프트, 이미지 내역이 더 보기 좋게 표시된다.   
 
<그림 3> 개선된 애플리케이션 레이아웃 ⓒ IDG

마지막 개선으로, 이미지를 클릭해 프롬프트와 함께 스테이블 디퓨전의 img2img 엔드포인트로 보내는 기능을 추가해 보자. 이렇게 하면 기존 이미지를 클릭해서 새 출력 이미지 URL을 위한 프롬프트와 결합할 수 있다. 클릭하면 이미지는 JSON 본문에 클릭한 이미지의 URL과 추가 필드 init_image가 포함된 요청을 생성한다. <예시 4>에서 이미지 클릭 핸들러와 업데이트된 generateImage() 함수가 있는 업데이트된 코드를 볼 수 있다. 

<예시 4> 업데이트된 코드 
const generateImage = async (imageUrl) => {
  const apiKey = "YOUR KEY HERE";
  let url = "https://stablediffusionapi.com/api/v3/text2img";
  const proxyUrl = "https://cors-anywhere.herokuapp.com/";

  const requestBody = {
    key: apiKey,
    prompt: textPrompt,
  };

  if (imageUrl) {
    requestBody.init_image = imageUrl;
    requestBody.samples = 1;
    requestBody.width = 800; 
    requestBody.height = 800;
    url = "https://stablediffusionapi.com/api/v3/img2img";
  }

  const requestOptions = {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(requestBody),
  };

  try {
    setIsLoading(true);
    const response = await fetch(proxyUrl + url, requestOptions);
    const data = await response.json();

    if (data.status === "error") {
      console.error("Error generating image: " + data.message);
      alert(data.message);
      return;
    }
    // Get the image URL from the response
    const generatedImageUrl = data.output[0];

    // Update the prompts array with the generated image URL
    setPrompts([...prompts, { prompt: textPrompt + (imageUrl ? " (image2image)" : ""), imageUrl: generatedImageUrl }]);
  } catch (error) {
    console.error("Error generating image:", error);
  } finally {
    setIsLoading(false);
  }
};

//...
const handleImageClick = (imageUrl) => {
  console.log("Clicked on image:", imageUrl);
        generateImage(imageUrl);
};

// ...
<img onClick={() => handleImageClick(item.imageUrl)} onerror="removeImage($(this));" src={item.imageUrl} alt="Generated Image" className="generated-image"/>

여기서 우리가 하는 주된 작업은 이미지에 클릭 핸들러를 추가하고 generateImage()에 imageUrl 인수가 있으면 image2image URL을 사용하고 필요한 인수를 JSON 본문에 추가하는 것이다(이 엔드포인트에는 샘플, 너비, 높이 매개변수가 필요함). 이제 이미지를 클릭해서 프롬프트를 추가해 <그림 4>와 같이 이미지를 발전시킬 수 있다.  
 
<그림 4> 이미지 프롬프트 ⓒ IDG
 

결론 

생성형 AI는 지난 1년에 걸쳐 큰 인기를 끌었고 스테이블 디퓨전은 텍스트 프롬프트를 기반으로 이미지를 생성하는 강력한 기능을 통해 중요한 모델 중 하나로 올라섰다. 지금까지 살펴본 것처럼 호스팅되는 API를 사용해 모델을 직접 설치하고 학습시키지 않고도 웹 애플리케이션에서 AI가 생성한 이미지를 이용할 수 있다.
editor@itworld.co.kr
Sponsored

회사명 : 한국IDG | 제호: ITWorld | 주소 : 서울시 중구 세종대로 23, 4층 우)04512
| 등록번호 : 서울 아00743 등록발행일자 : 2009년 01월 19일

발행인 : 박형미 | 편집인 : 박재곤 | 청소년보호책임자 : 한정규
| 사업자 등록번호 : 214-87-22467 Tel : 02-558-6950

Copyright © 2024 International Data Group. All rights reserved.