본문 바로가기
REFLEX 튜토리얼

[pynecone] Dall-E 2 이미지 생성 앱 만들기⑤ #마치며

by 일코 2023. 2. 16.

파인콘으로 이미지생성앱 만들기
마지막 시간입니다.

지난 포스팅에서 사실상 앱을 완성한 거나 다름이 없었는데,

2023.02.16 - [pynecone 튜토리얼] - [pynecone] Dall-E 2 이미지 생성 앱 만들기④

 

[pynecone] Dall-E 2 이미지 생성 앱 만들기④

들어가기 전에 지난 시간에는 작정하고 우리 앱에 들어가는 파인콘 컴포넌트 10개를 모두 짚어보았습니다. 사실 컴포넌트가 어떻게 작동하고 어떤 속성을 지녔는지를 파악했다면, 이들을 배치

martinii.fun

이번 포스팅에서는 세 가지 정도만 다듬어보겠습니다.
바로,

① 프로그레스바는 이미지 생성중에만 나타나게 하기(pc.cond 활용)
② 프롬프트 하나로 이미지 여러 개 한 번에 생성하기
③ API 오류 발생시 window_alert으로 팝업 띄워주기

입니다.

잠시 호흡을 가다듬고,
...시작해봅시다^^

① 프로그레스바 관련

우리가 삽입한 원형 프로그레스바는
이미지 생성중에만 잠깐 나타나게 바꿔주려고 합니다.
프롬프트 전송 전이나 이미지 생성 후에는 사라지게 하려는 거죠.

그러려면 먼저 프롬프트 전송 직후부터 이미지 생성 시점까지만
값이 True인 변수를 하나 더 사용해야 할 것 같습니다.

만들어만 놓고 아직 사용하지는 않았던 State.진행중 변수입니다!

State.진행중 == True인 시간은,
버튼 클릭 직후부터 ~ 이미지 생성 완료시점까지만이니까

(먼저 충분히 고민해보시고..)
State 클래스를 아래와 같이 수정해봅시다.
화살표 부분에 특히 집중해 주시기 바랍니다.

class State(pc.State):
    prompt = ""
    image_url = ""
    진행중 = False  # <--
    완료 = False

    def process(self):  # <--
        self.진행중 = True  # <-- 프로그레스바 나타나게!
        self.완료 = False  # <-- 다음 프롬프트 실행시 이미지 사라지게!
    
    def get_image(self):
        response = openai.Image.create(prompt=self.prompt, n=1, size="256x256")
        self.image_url = response["data"][0]["url"]
        self.진행중 = False  # 프로그레스바 사라지게!  # <--
        self.완료 = True  # 이미지 나타나게!

수정은 제법 간단했습니다.

process 함수를 만든 이유는
바로 pc.button의 on_click에 연결하기 위함인데요.

아닛! pc.button의 on_click에는
이미 State.get_image가 연결되어 있는데요!??

괜찮습니다.
이벤트리스너에는
두 개 이상의 함수도 간단히 연결할 수 있습니다.
바로 리스트(또는 튜플)로 묶으면 됩니다.

pc.button 코드를 아래와 같이 수정해줍니다.

pc.button("Generate Image", width="100%", on_click=[State.process, State.get_image]),
#                                                  --------------------------------

 

마지막으로 pc.circular_progress 부분을
아래처럼 pc.cond로 감싸줍니다.

pc.cond(State.진행중, pc.circular_progress(is_indeterminate=True)),

그리고 다시 웹앱을 열어보면

이제 적절한 타이밍에만 프로그레스바가 나타나게 변경되었습니다. 오예!

그럼 마지막으로
이미지를 한 여덟 장쯤 동시에 생성하게
앱을 바꿔볼까요?

보나마나 방법은 간단하겠죠?
① API 명령어 안에 n 값을 1에서 8로 바꾸고,

response = openai.Image.create(prompt=self.prompt, n=8, size="256x256")
#                                                  ---


② image_url 한 개만 처리했던 것을 image_url0 ~ image_url7 등 여덟 개로 늘려주고

class State(pc.State):
    prompt = ""
    image_url0 = ""  # <---
    image_url1 = ""  # <---
    image_url2 = ""  # <---
    image_url3 = ""  # <---
    image_url4 = ""  # <---
    image_url5 = ""  # <---
    image_url6 = ""  # <---
    image_url7 = ""  # <---
    진행중 = False
    완료 = False

    def on_process(self):
        self.진행중 = True  # 프로그레스바 나타나게!
        self.완료 = False  # 다음 프롬프트 실행시 이미지 사라지게!

    def get_image(self):
        response = openai.Image.create(prompt=self.prompt, n=1, size="256x256")
        self.image_url0 = response["data"][0]["url"]  # <---
        self.image_url1 = response["data"][1]["url"]  # <---
        self.image_url2 = response["data"][2]["url"]  # <---
        self.image_url3 = response["data"][3]["url"]  # <---
        self.image_url4 = response["data"][4]["url"]  # <---
        self.image_url5 = response["data"][5]["url"]  # <---
        self.image_url6 = response["data"][6]["url"]  # <---
        self.image_url7 = response["data"][7]["url"]  # <---
        self.진행중 = False  # 프로그레스바 사라지게!
        self.완료 = True  # 이미지 나타나게!


③ 마지막으로 pc.image를 pc.vstack과 pc.hstack을 적절히 섞어 여덟개 만들어주기

pc.vstack(
    pc.hstack(
        pc.image(src=State.image_url0, height="10em", width="10em"),
        pc.image(src=State.image_url1, height="10em", width="10em"),
        pc.image(src=State.image_url2, height="10em", width="10em"),
        pc.image(src=State.image_url3, height="10em", width="10em"),
    ),
    pc.hstack(
        pc.image(src=State.image_url4, height="10em", width="10em"),
        pc.image(src=State.image_url5, height="10em", width="10em"),
        pc.image(src=State.image_url6, height="10em", width="10em"),
        pc.image(src=State.image_url7, height="10em", width="10em"),
    ),
),
미묘하게 노가다 같은 구석이 있어서 코드를 수정해보고 싶은 분들도 있을 거예요.
예를 들면 리스트 언패킹이나 pc.foreach 같은 걸로 말이죠.
(튜토리얼 작성 시점인 pynecone 0.1.14 버전 기준으로)
아직 파이썬의 리스트 자료형은
State 클래스와 index 함수간의 전송을
지원하지 않고 있는 것 같아요.

더 효율적으로 코딩할 수 있는 방법을 알게 되면
포스팅을 업데이트 해 둘 게요.
정말 이 세 개만 수정하면
되는 건가요?
이렇게 간단해도 되나요?

예, 됩니다.

위의 코드를 다 수정하셨으면
우리 전체 코드는 아래와 같은 상태가 됩니다.
여러분의 코드와 비교해 보시길 바랍니다.

import openai
import pynecone as pc

openai.api_key = "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"  # 여러분의 API키로 수정


class State(pc.State):
    prompt = ""
    image_url0 = ""
    image_url1 = ""
    image_url2 = ""
    image_url3 = ""
    image_url4 = ""
    image_url5 = ""
    image_url6 = ""
    image_url7 = ""
    진행중 = False
    완료 = False

    def on_process(self):
        self.진행중 = True  # 프로그레스바 나타나게!
        self.완료 = False  # 다음 프롬프트 실행시 이미지 사라지게!

    def get_image(self):
        response = openai.Image.create(prompt=self.prompt, n=8, size="256x256")
        self.image_url0 = response["data"][0]["url"]
        self.image_url1 = response["data"][1]["url"]
        self.image_url2 = response["data"][2]["url"]
        self.image_url3 = response["data"][3]["url"]
        self.image_url4 = response["data"][4]["url"]
        self.image_url5 = response["data"][5]["url"]
        self.image_url6 = response["data"][6]["url"]
        self.image_url7 = response["data"][7]["url"]
        self.진행중 = False  # 프로그레스바 사라지게!
        self.완료 = True  # 이미지 나타나게!


def index():
    return pc.center(
        pc.vstack(
            pc.heading("DALL-E", font_size="1.5em"),
            pc.input(placeholder="Enter a prompt..", on_blur=State.set_prompt),
            pc.button("Generate Image", width="100%", on_click=[State.on_process, State.get_image]),
            pc.divider(),
            pc.cond(State.진행중, pc.circular_progress(is_indeterminate=True)),
            pc.cond(State.완료,
                    pc.vstack(
                        pc.hstack(
                            pc.image(src=State.image_url0, height="10em", width="10em", ),
                            pc.image(src=State.image_url1, height="10em", width="10em", ),
                            pc.image(src=State.image_url2, height="10em", width="10em", ),
                            pc.image(src=State.image_url3, height="10em", width="10em", ),
                        ),
                        pc.hstack(
                            pc.image(src=State.image_url4, height="10em", width="10em", ),
                            pc.image(src=State.image_url5, height="10em", width="10em", ),
                            pc.image(src=State.image_url6, height="10em", width="10em", ),
                            pc.image(src=State.image_url7, height="10em", width="10em", ),
                        ),
                    ),
                    ),
            bg="white", padding="2em", shadow="lg", border_radius="lg",
        ),
        width="100%", height="100vh",
        background="radial-gradient(circle at 22% 11%,rgba(62, 180, 137,.20),hsla(0,0%,100%,0) 19%),radial-gradient(circle at 82% 25%,rgba(33,150,243,.18),hsla(0,0%,100%,0) 35%),radial-gradient(circle at 25% 61%,rgba(250, 128, 114, .28),hsla(0,0%,100%,0) 55%)",
    )


app = pc.App(state=State)
app.add_page(index, title="Pynecone:DALL-E")
app.compile()

 

잘 작동하는지 앱으로 이동해서
확인해볼까요?

잘 변경되었네요!

마지막으로
세 번째 항목인 window_alert은

아래와 같은 상세한 오류메시지를 브라우저에 띄워주기 위함인데요.

실제 우리가 서비스를 배포했을 때,
확인할 수도 없는 "보다 자세한 내용은 로그 메시지를 확인하세요."
이런 메시지가 뜨면 황당하겠죠.

그래서 openai API에서 제공하는 오류메시지를 브라우저 경고창으로 보여주려고 해요.

get_image 메서드를 아래와 같이 수정합니다.
(오류가 발생하는 경우에만 경고창을 띄우는 명령어를 실행하려고 해요.)

def get_image(self):
    try:  # <--
        response = openai.Image.create(prompt=self.prompt, n=8, size="256x256")
        self.image_url0 = response["data"][0]["url"]
        self.image_url1 = response["data"][1]["url"]
        self.image_url2 = response["data"][2]["url"]
        self.image_url3 = response["data"][3]["url"]
        self.image_url4 = response["data"][4]["url"]
        self.image_url5 = response["data"][5]["url"]
        self.image_url6 = response["data"][6]["url"]
        self.image_url7 = response["data"][7]["url"]
        self.진행중 = False  # 프로그레스바 사라지게!
        self.완료 = True  # 이미지 나타나게!
    except (openai.error.InvalidRequestError, openai.error.RateLimitError) as e:  # <--
        return pc.window_alert(str(e))  # <--

예를 들어 1분에 50회 이상 요청을 던질 경우(prompt가 아닌 n 기준)
RateLimitError를 뱉고,
불건전(예:nude)하거나 부적절한(예:Joe Biden) 프롬프트의 경우에도
InvalidRequestError를 뱉습니다.

위와 같이 코드를 수정하셨으면
마지막으로 RateLimitError를 한 번 만나보고
튜토리얼을 마치겠습니다.
한 10번 버튼을 연속으로 눌러주면
아래와 같이 상세한 오류메시지가 뜨게 됩니다.

여기서 마치면 될 것 같네요.
긴 튜토리얼 따라와주시느라 수고하셨습니다.
다음엔 더 재미있고 유익한 튜토리얼로 뵙고 싶네요.

마치며

pynecone 프레임워크는 포스팅 작성시점인 현재
아직도 알파 릴리즈 상태이고
크고작은 버그들이 산재해 있는 실정입니다.
주요기능도 계속해서 추가하고 있고요.
그럼에도 이번 튜토리얼처럼
React.js나 Next.js, FastAPI 등에 대해 전혀 모르는 상태에서도
이 정도 퀄리티의 웹앱을 코드 70여줄에
작성할 수 있다는 데에는
대단하다 못해 굉장하다는 느낌입니다.
(물론 다수의 개발자 분들에게는 덕지덕지 래핑의 문제점이 커 보일 수도 있겠지만요)

저와 같은 후루꾸 시민개발자,
UI라이브러리나 웹프레임워크에 대해 깊이 공부할 시간은 없지만
뭔가 내 성과가 담긴 앱이나 분석결과를 웹에 공유하고 싶을 때

pynecone이 제법 괜찮은 대안이 되어줄 거라고 생각합니다.

행복한 하루 되세요!

일상의코딩,
일코 드림


pynecone의 다른 튜토리얼

2023.01.29 - [pynecone 튜토리얼] - [pynecone] 리액트 컴포넌트를 파인콘에서 사용하기 #react-colorful

 

[pynecone] 리액트 컴포넌트를 파인콘에서 사용하기 #react-colorful

이번 포스팅에서 파인콘에 집어넣어 볼 리액트 컴포넌트는 react-colorful 입니다. omgovich/react-colorful: 🎨 A tiny (2,8 KB) color picker component for React and Preact apps (github.com) GitHub - omgovich/react-colorful: 🎨 A ti

martinii.fun

2023.01.17 - [pynecone 튜토리얼] - 파인콘 첫 번째 예제, 카운터 앱

 

파인콘 첫 번째 예제, 카운터 앱

카운터 앱 만들어보기 파인콘의 공식문서에서 제공하고 있는 첫 번째 예제, 카운터 앱을 같이 만들어봅시다. 전체 소스코드와 영문 설명은 아래 링크의 문서에서 확인하실 수 있습니다. https://py

martinii.fun

 

일코의 python+hwp 업무자동화 강의

https://inf.run/FGz5

 

움짤로 빠르게 배우는 파이썬-아래아한글 자동화 레시피 - 인프런 | 강의

파이썬으로 아래아한글을 다루는 짧은 예제코드들을 소개하고, 중간중간의 결과를 GIF로 보여드립니다. 동영상 강의가 아니지만 오히려 빠르게 배울 수 있고, 따라하기도 쉽습니다., - 강의 소개

www.inflearn.com

# 16,000원 할인쿠폰 : 9063-37a0c98ae02b [2023. 2월말까지]

댓글