Android Studio - OpenAI(dall-e-3)

Minjae Lee·2024년 2월 26일

Android Studio

목록 보기
5/12

0. workflow

필자는 아래와 같이 주어진 스크립트에 대해 이미지를 생성하는 dall-e-3를 활용해보고자 한다. 아래 사진의 경우 '컴퓨터 배경화면으로 사용할 그림 그려줘'라는 요청을 보냈다.

0-1. 기본 설정

  • 외부 모듈 implementation
  • 권한 부여
  • icon 추가

0-2. dall-e-3와 응답 주고받기

  • dall-e-3
  • 스크립트를 전송할 수 있는 xml 페이지 생성
  • Open AI API Key 발급받기
  • dall-e-3와 통신하여 imageView에 생성된 이미지 띄우기

1. 기본 설정

1-1. 외부 모듈 implementation

  • build.gradle (app)
dependencies {
    ...

    implementation 'com.squareup.okhttp3:okhttp:4.10.0'
}

build.gradle 수정 후 오른쪽 상단 위에 Sync now 클릭

1-2. 권한 부여

dall-e와 응답을 주고받기 위해서 통신을 해야 하므로, 인터넷 권한을 부여해줘야 한다.

  • AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <uses-permission android:name="android.permission.INTERNET" />
  
	...
</manifest>

1-3. icon 추가 (선택)

응답을 보내기 위한 icon을 추가하였다.
왼쪽 탭의 Resource Manager > + 버튼 > Import Drawables 누른 후, 다운받은 이미지 추가 > Next 버튼 > Import 버튼 차례로 클릭
Resource Manager에 이미지(icon_send) 추가되었는지 확인

2. dall-e-3와 응답 주고받기

2-0. dall-e-3

GPT API 공식문서에 따르면, dall-e-3의 request format은 아래와 같다.

curl https://api.openai.com/v1/images/generations \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $OPENAI_API_KEY" \
  -d '{
    "model": "dall-e-3",
    "prompt": "A cute baby sea otter",
    "n": 1,
    "size": "1024x1024"
  }'

위의 형식을 살펴보면, Content-TypeAuthorization을 지정해주고, modelprompt, n, size로 구성된 json을 가지고 통신한다는 것을 알 수 있다. 이 때, Authorization$OPENAI_API_KEY가 사용자의 API Key에 해당한다.
또한, prompt가 이미지를 생성할 스크립트, n이 이미지의 개수(dall-e-3는 반드시 n이 1이어야 한다), size는 이미지의 크기를 의미한다.
dall-e-3의 response format은 아래와 같다.

{
  "created": 1589478378,
  "data": [
    {
      "url": "https://..."
    },
    {
      "url": "https://..."
    }
  ]
}

dall-e-3는 생성된 이미지를 url 형태로 전달하기 때문에, 전달받은 json으로부터 url을 추출하고, 이를 ImageView에 띄우면 성공적으로 앱의 사용자에게 이미지가 보여지게 된다.

2-1. 스크립트를 전송할 수 있는 xml 페이지 생성

TextView와 EditText, ImageView를 적절히 배치하여 채팅을 주고받을 수 있는 xml 파일을 생성한다.
EditText의 속성으로 inputType="textMultiLine"과 maxLines="5"를 추가하여 실제 카톡처럼 여러줄을 입력하면 EditText의 높이가 높아지도록 하였다.

  • activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <EditText
        android:id="@+id/requestEditText"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginLeft="16dp"
        android:layout_marginRight="16dp"
        android:layout_marginBottom="16dp"
        android:background="@drawable/round_button"
        android:ems="10"
        android:hint="write here"
        android:inputType="textMultiLine"
        android:maxLines="5"
        android:padding="10dp"
        android:privateImeOptions="defaultInputmode=korean"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toStartOf="@+id/sendBtn"
        app:layout_constraintStart_toStartOf="parent" />

    <ImageView
        android:id="@+id/sendBtn"
        android:layout_width="40dp"
        android:layout_height="40dp"
        android:layout_marginEnd="16dp"
        android:layout_marginBottom="16dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:srcCompat="@drawable/icon_send" />

    <ImageView
        android:id="@+id/responseImg"
        android:layout_width="300dp"
        android:layout_height="300dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        tools:srcCompat="@tools:sample/avatars" />

    <TextView
        android:id="@+id/showText"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="스크립트를 입력해주세요"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.498"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_bias="0.755" />

</androidx.constraintlayout.widget.ConstraintLayout>

2-2. Open AI API Key 발급받기

chatGPT를 사용하기 위해선 API key를 발급받아야 한다.
GPT-API key 사이트로 접속하여 Create new secret key 버튼을 클릭하여 API key를 발급받는다. 발급받은 key는 다시 확인할 수 없으니 메모장에 반드시 복사-붙여넣기 해야한다.
또한, key를 발급받아도 결제를 하지 않으면 GPT와 응답을 주고받을 수 없으므로 GPT billing 사이트로 접속하여 add payment details 버튼을 클릭하여 카드를 등록하고, 기본요금 $5.5를 결제한다.

2-3. dall-e-3와 통신하여 imageView에 생성된 이미지 띄우기

해당 과정에서 구현해야 할 것은 크게 아래와 같이 구분할 수 있다.
1. onCreate함수
2. showText함수
3. setImg함수
4. callAPI함수
5. showImage함수

먼저 onCreate함수에서는 앞서 설정한 send 버튼을 클릭 시 editText의 내용을 지움과 동시에 dall-e-3에게 응답을 보내는 코드를 구현한다. 아래의 코드에서 callAPI함수는 나중에 구현한다.
또한, client는 http 통신을 위해 반드시 구현되어야 한다.

protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    editText = findViewById(R.id.requestEditText);
    sendBtn = findViewById(R.id.sendBtn);
    responseImg = findViewById(R.id.responseImg);
    textView = findViewById(R.id.showText);

    sendBtn.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            System.out.println(editText.getText());
            String question = editText.getText().toString().trim();

            editText.setText("");
            callAPI(question);
        }
    });

    client = new OkHttpClient().newBuilder()
            .connectTimeout(60, TimeUnit.SECONDS)
            .writeTimeout(120, TimeUnit.SECONDS)
            .readTimeout(60, TimeUnit.SECONDS)
            .build();
}

다음으로 showText함수를 구현한다. 해당 함수는 dall-e-3와 통신하는 중에 띄울 안내 메시지를 위해 구현해야 한다. 예를 들면, dall-e-3로부터 전달받은 url을 다운받을 때 "이미지 다운로드 중..."과 같은 안내 문구를 띄운다. 이 함수는 runOnUiThread함수를 이용하여 다른 thread에서도 view에 접근 및 수정이 가능하다(showText함수 없이 바로 추가할 경우 에러 발생).

void showText(String string) {
    runOnUiThread(new Runnable() {
        @Override
        public void run() {
            textView.setText(string);
        }
    });
}

setImg함수는 dall-e-3으로부터 다운받은 Bitmap 이미지를 ImageView에 띄울 때 사용하는 함수이다. 이 함수도 showText함수와 마찬가지로 runOnUiThread함수를 이용하여 다른 thread에서도 view에 접근 및 수정이 가능하게 했다.

void setImg(Bitmap bitmap) {
    runOnUiThread(new Runnable() {
        @Override
        public void run() {
            responseImg.setImageBitmap(bitmap);
        }
    });
}

callAPI함수는 dall-e-3에게 응답을 보내고, 받아오는 역할을 한다. 위에서 살펴봤던 request format에 맞추어 JSONObject를 구성하고, 요청을 보내면 된다. client의 Callback 함수에서 jsonArray.getJSONObject(0).getString("url")부분이 dall-e-3의 response format에 맞추어 답변 내용을 가져오는 코드이다.

void callAPI(String question){
    JSONObject object = new JSONObject();
    try {
        object.put("model", "dall-e-3");
        object.put("prompt", question);
        object.put("size", "1024x1024");

    } catch (JSONException e){
        e.printStackTrace();
    }

    RequestBody body = RequestBody.create(object.toString(), JSON);
    Request request = new Request.Builder()
            .url("https://api.openai.com/v1/images/generations")
            .header("Authorization", "Bearer "+MY_SECRET_KEY)
            .post(body)
            .build();

    client.newCall(request).enqueue(new Callback() {
        @Override
        public void onFailure(@NonNull Call call, @NonNull IOException e) {
            showText("이미지 생성 실패\n" + e.getMessage());
        }

        @Override
        public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {
            if (response.isSuccessful()) {
                JSONObject jsonObject = null;
                try {
                    jsonObject = new JSONObject(response.body().string());
                    JSONArray jsonArray = jsonObject.getJSONArray("data");

                    String result = jsonArray.getJSONObject(0).getString("url");
                    showText("이미지 다운로드 중...");
                    showImage(result);
                } catch (JSONException e) {
                    e.printStackTrace();
                }
            } else {
                showText("이미지 생성 실패\n" + response.body().string());
            }
        }
    });
}

마지막으로 showImage함수는 dall-e-3로부터 전달받은 url을 바탕으로 이미지를 다운받아 ImageView에 띄우는 역할을 하는 함수이다. 이미지를 다운받을 수 있는 Thread를 하나 생성하고, Thread가 종료될 때까지 기다린다(while문을 통한 busy waiting). 이후 Thread가 종료된다면 그 때 Bitmap을 ImageView에 띄운다.

public void showImage(String urlResponse){
        showText("이미지 다운로드 중...");

        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    URL url = new URL(urlResponse);
                    bitmapOutputImage = BitmapFactory.decodeStream(url.openStream());
                } catch (MalformedURLException e) {
                    throw new RuntimeException(e);
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
        });

        thread.start();

        while (thread.isAlive()) { }

        Bitmap bitmapFinalImage = Bitmap.createScaledBitmap(bitmapOutputImage,
                responseImg.getWidth(), responseImg.getHeight(), true);
        setImg(bitmapFinalImage);
        showText("이미지 다운로드 완료");
    }

전체 코드는 아래와 같다.

  • MainActivity.java
package com.example.chatgpt;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.RecyclerView;

import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Bundle;
import android.view.View;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.TextView;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.concurrent.TimeUnit;

import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;

public class MainActivity extends AppCompatActivity {

    EditText editText;
    ImageView sendBtn, responseImg;
    TextView textView;
    Bitmap bitmapOutputImage;
    public static  final MediaType JSON = MediaType.get("application/json; charset=utf-8");
    private static final String MY_SECRET_KEY = "your-api-key";
    OkHttpClient client;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        editText = findViewById(R.id.requestEditText);
        sendBtn = findViewById(R.id.sendBtn);
        responseImg = findViewById(R.id.responseImg);
        textView = findViewById(R.id.showText);

        sendBtn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                System.out.println(editText.getText());
                String question = editText.getText().toString().trim();

                editText.setText("");
                callAPI(question);
            }
        });

        client = new OkHttpClient().newBuilder()
                .connectTimeout(60, TimeUnit.SECONDS)
                .writeTimeout(120, TimeUnit.SECONDS)
                .readTimeout(60, TimeUnit.SECONDS)
                .build();
    }

    void showText(String string) {
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                textView.setText(string);
            }
        });
    }

    void setImg(Bitmap bitmap) {
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                responseImg.setImageBitmap(bitmap);
            }
        });
    }

    void callAPI(String question){
        JSONObject object = new JSONObject();
        try {
            object.put("model", "dall-e-3");
            object.put("prompt", question);
            object.put("size", "1024x1024");

        } catch (JSONException e){
            e.printStackTrace();
        }

        RequestBody body = RequestBody.create(object.toString(), JSON);
        Request request = new Request.Builder()
                .url("https://api.openai.com/v1/images/generations")
                .header("Authorization", "Bearer "+MY_SECRET_KEY)
                .post(body)
                .build();

        client.newCall(request).enqueue(new Callback() {
            @Override
            public void onFailure(@NonNull Call call, @NonNull IOException e) {
                showText("이미지 생성 실패\n" + e.getMessage());
            }

            @Override
            public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {
                if (response.isSuccessful()) {
                    JSONObject jsonObject = null;
                    try {
                        jsonObject = new JSONObject(response.body().string());
                        JSONArray jsonArray = jsonObject.getJSONArray("data");

                        String result = jsonArray.getJSONObject(0).getString("url");
                        showText("이미지 다운로드 중...");
                        showImage(result);
                    } catch (JSONException e) {
                        e.printStackTrace();
                    }
                } else {
                    showText("이미지 생성 실패\n" + response.body().string());
                }
            }
        });
    }

    public void showImage(String urlResponse){
        showText("이미지 다운로드 중...");

        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    URL url = new URL(urlResponse);
                    bitmapOutputImage = BitmapFactory.decodeStream(url.openStream());
                } catch (MalformedURLException e) {
                    throw new RuntimeException(e);
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
        });

        thread.start();

        while (thread.isAlive()) { }

        Bitmap bitmapFinalImage = Bitmap.createScaledBitmap(bitmapOutputImage,
                responseImg.getWidth(), responseImg.getHeight(), true);
        setImg(bitmapFinalImage);
        showText("이미지 다운로드 완료");
    }
}

3. 실행결과

앱을 실행시키면 아래와 같이 작동하게 된다. 아래 사진의 경우 '컴퓨터 배경화면으로 사용할 그림 그려줘'라는 요청을 보냈다.

0개의 댓글