Spaces:
Sleeping
Sleeping
Kimilhee
commited on
Commit
·
db315f2
1
Parent(s):
ed8900a
파일 분리 리팩토링.
Browse files- README.md +69 -4
- app.py +18 -900
- config.py +101 -0
- image_generator.py +190 -0
- requirements.txt +12 -11
- ui/__init__.py +7 -0
- ui/css/base.css +8 -0
- ui/css/fade-in.css +22 -0
- ui/css/fade-out.css +46 -0
- ui/css/fade-visibility.css +20 -0
- ui/css/interactions.css +12 -0
- ui/image_gen_tab.py +248 -0
- ui/styles.py +53 -0
- ui/worldcup_tab.py +185 -0
- worldcup.py +62 -0
README.md
CHANGED
@@ -1,5 +1,5 @@
|
|
1 |
---
|
2 |
-
title:
|
3 |
emoji: 🐢
|
4 |
colorFrom: yellow
|
5 |
colorTo: green
|
@@ -7,12 +7,77 @@ sdk: gradio
|
|
7 |
sdk_version: 5.20.0
|
8 |
app_file: app.py
|
9 |
pinned: false
|
10 |
-
short_description:
|
11 |
---
|
12 |
|
13 |
-
|
14 |
|
15 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
16 |
|
17 |
```bash
|
18 |
uvicorn main:app --host 0.0.0.0 --port 8000 --reload
|
|
|
1 |
---
|
2 |
+
title: 대교 AI 콘텐츠 이미지 월드컵
|
3 |
emoji: 🐢
|
4 |
colorFrom: yellow
|
5 |
colorTo: green
|
|
|
7 |
sdk_version: 5.20.0
|
8 |
app_file: app.py
|
9 |
pinned: false
|
10 |
+
short_description: 대교 AI 콘텐츠 이미지 생성 및 월드컵 서비스
|
11 |
---
|
12 |
|
13 |
+
# 대교 AI 콘텐츠 이미지 월드컵
|
14 |
|
15 |
+
이 애플리케이션은 사용자가 교재 지문을 입력하면 AI를 활용하여 이미지를 생성하고, 생성된 이미지들로 월드컵 형식의 대결을 진행할 수 있는 서비스입니다.
|
16 |
+
|
17 |
+
## 프로젝트 구조
|
18 |
+
|
19 |
+
```
|
20 |
+
.
|
21 |
+
├── app.py # 메인 애플리케이션 파일
|
22 |
+
├── config.py # 설정 및 상수 정의
|
23 |
+
├── image_generator.py # 이미지 생성 관련 기능
|
24 |
+
├── models.py # 데이터 모델 정의
|
25 |
+
├── requirements.txt # 필요한 패키지 목록
|
26 |
+
├── ui/ # UI 컴포넌트
|
27 |
+
│ ├── __init__.py # UI 패키지 초기화
|
28 |
+
│ ├── image_creation.py # 이미지 생성 UI
|
29 |
+
│ ├── styles.py # UI 스타일 정의
|
30 |
+
│ └── worldcup.py # 월드컵 기능 UI
|
31 |
+
└── static/ # 정적 파일 (이미지 등)
|
32 |
+
├── beauty/ # 예제 이미지
|
33 |
+
├── examples/ # 예제 이미지
|
34 |
+
└── image/ # UI에 사용되는 이미지
|
35 |
+
```
|
36 |
+
|
37 |
+
## 기능
|
38 |
+
|
39 |
+
1. **이미지 생성**: 교재 지문을 입력하면 AI를 활용하여 관련 이미지를 생성합니다.
|
40 |
+
|
41 |
+
- 다양한 이미지 스타일 선택 가능
|
42 |
+
- 이미지 비율 조정 가능
|
43 |
+
- 추가 희망사항 입력 가능
|
44 |
+
|
45 |
+
2. **이미지 월드컵**: 생성된 이미지들을 토너먼트 방식으로 비교하여 최종 우승 이미지를 선정합니다.
|
46 |
+
- 4강, 8강, 16강 형식 지원
|
47 |
+
- 직관적인 UI로 쉽게 이미지 선택 가능
|
48 |
+
|
49 |
+
## 서버 실행 방법
|
50 |
+
|
51 |
+
```bash
|
52 |
+
# 개발 모드
|
53 |
+
uvicorn main:app --host 0.0.0.0 --port 8000 --reload
|
54 |
+
|
55 |
+
# 서비스 실행
|
56 |
+
./run-service.sh
|
57 |
+
|
58 |
+
# 로그 확인
|
59 |
+
tail -f nohup.out
|
60 |
+
|
61 |
+
# 로그 파일 초기화
|
62 |
+
> nohup.out
|
63 |
+
```
|
64 |
+
|
65 |
+
## 개발 환경 설정
|
66 |
+
|
67 |
+
```bash
|
68 |
+
# 가상 환경 생성 및 활성화
|
69 |
+
python -m venv venv
|
70 |
+
source venv/bin/activate # Linux/Mac
|
71 |
+
# 또는
|
72 |
+
venv\Scripts\activate # Windows
|
73 |
+
|
74 |
+
# 의존성 설치
|
75 |
+
pip install -r requirements.txt
|
76 |
+
```
|
77 |
+
|
78 |
+
## 추가 정보
|
79 |
+
|
80 |
+
Hugging Face 설정 참조: https://huggingface.co/docs/hub/spaces-config-reference
|
81 |
|
82 |
```bash
|
83 |
uvicorn main:app --host 0.0.0.0 --port 8000 --reload
|
app.py
CHANGED
@@ -1,923 +1,41 @@
|
|
1 |
-
from dotenv import load_dotenv
|
2 |
-
from dataclasses import dataclass
|
3 |
-
from random import shuffle
|
4 |
-
import os
|
5 |
-
import gradio as gr
|
6 |
-
import replicate
|
7 |
-
import time
|
8 |
-
|
9 |
-
# .env 파일 로드
|
10 |
-
load_dotenv()
|
11 |
-
|
12 |
-
gr.set_static_paths(paths=["static/"])
|
13 |
-
|
14 |
-
|
15 |
-
def to_string(output):
|
16 |
-
"""
|
17 |
-
output 을 하나의 문자열로 합치기.
|
18 |
-
"""
|
19 |
-
combined_result = ""
|
20 |
-
for item in output:
|
21 |
-
combined_result += str(item)
|
22 |
-
return combined_result
|
23 |
-
|
24 |
-
|
25 |
-
FINAL_ROUND = "## <center>결승전</center>"
|
26 |
-
VS_IMAGE = "<img src='/gradio_api/file=static/image/vs.png' style='margin-top: 100px;'>"
|
27 |
-
WORLDCUPABLE_IMAGE_CNT = [4, 8, 16]
|
28 |
-
|
29 |
-
|
30 |
-
@dataclass
|
31 |
-
class Worldcup:
|
32 |
-
"""
|
33 |
-
이미지 월드컵 이미지 리스트를 받아서 참가 이미지들 리스트와 다음 승리자 리스트(다음 라운드 출전 리스트)를 가지고 있는 클래스.
|
34 |
-
이 클래스는 다음 메소드들을 가지고 있음.
|
35 |
-
getCurrentRoundImages (다음 대전 이미지 2개를 리스트로 반환함.)
|
36 |
-
winImage (0 또는 1을 입력 받아 승리자 리스트에 추가하고 다음 라운드를 위해 index를 증가시킴.)
|
37 |
-
처음에는 8강 이미지 (8개) 가 주어지고, 이 이미지들 중 getCurrentRoundImages 를 호출해서 그 둘 중 이긴 이미지에 대한 피드백을 winImage 에 전달하고, 이긴 이미지를 승리자 리스트에 추가함.
|
38 |
-
이렇게 해서 8강, 4강, 결승전 이미지들을 반환하며 대전을 진행할 수 있는 클래스
|
39 |
-
|
40 |
-
사용 예시:
|
41 |
-
worldcup = Worldcup(images=이미지_리스트)
|
42 |
-
"""
|
43 |
-
|
44 |
-
images: list
|
45 |
-
winners: list = None
|
46 |
-
index: int = 0
|
47 |
-
|
48 |
-
def __post_init__(self):
|
49 |
-
if self.winners is None:
|
50 |
-
self.winners = []
|
51 |
-
|
52 |
-
def getCurrentRoundImages(self):
|
53 |
-
# 현재 라운드의 이미지가 모두 소진되었는지 확인
|
54 |
-
# 다음 대전할 이미지 2개 반환
|
55 |
-
return self.images[self.index : self.index + 2]
|
56 |
-
|
57 |
-
def winImage(self, winnerIndex):
|
58 |
-
print("winImage winnerIndex:", winnerIndex)
|
59 |
-
self.winners.append(self.images[self.index + winnerIndex])
|
60 |
-
self.index += 2
|
61 |
-
|
62 |
-
if self.getKangRound() == FINAL_ROUND:
|
63 |
-
return self.winners[0]
|
64 |
-
|
65 |
-
if self.index >= len(self.images):
|
66 |
-
print("winImage 다음 라운드로 진행", self.index, len(self.images))
|
67 |
-
# 다음 라운드로 진행: winners를 새로운 images로 설정
|
68 |
-
self.images = self.winners
|
69 |
-
self.winners = []
|
70 |
-
self.index = 0
|
71 |
-
|
72 |
-
# 현재 몇강인지 가져오는 함수
|
73 |
-
def getKang(self):
|
74 |
-
return len(self.images)
|
75 |
-
|
76 |
-
# 현재 라운드 가져오는 함수 (8강 2차전 할 때, 2에 해당 하는 값)
|
77 |
-
def getRound(self):
|
78 |
-
return self.index // 2 + 1
|
79 |
-
|
80 |
-
def getKangRound(self):
|
81 |
-
if self.getKang() == 2:
|
82 |
-
return FINAL_ROUND
|
83 |
-
return f"## <center>{self.getKang()}강 {self.getRound()}차전</center>"
|
84 |
-
|
85 |
-
|
86 |
-
imageStyleMap = {
|
87 |
-
"알아서": "Something that you think is the best.",
|
88 |
-
"실사": "photo-realistic",
|
89 |
-
"2D애니": "Classic 90s Disney animation style, 2D hand-drawn aesthetic, rich color palette, traditional cel animation look, expressive character design, detailed painted backgrounds, dramatic lighting, Disney Renaissance era, vibrant scenery, soft shadows, cinematic composition, animated features, warm color tones, characterized by Glen Keane and Mark Henn artistic influence, Disney traditional animation techniques, theatrical quality.",
|
90 |
-
"일본2D애니": "Studio Ghibli style, Hayao Miyazaki aesthetic, hand-drawn animation, whimsical natural environments, soft pastel color palette, detailed background art, nostalgic lighting, ethereal atmosphere, fantastical elements blended with realism, delicate line work",
|
91 |
-
"3D애니메이션": "3d_animation, Disney Pixar",
|
92 |
-
"웹툰": "webtoon, manga",
|
93 |
-
"일러스트": "illustration, Hand-drawn art, Artwork",
|
94 |
-
}
|
95 |
-
|
96 |
-
HIGH_PRICE_OPTION = "비싼거(60원/장)"
|
97 |
-
LOW_PRICE_OPTION = "싼거(5원/장)"
|
98 |
-
HIGH_PRICE = 60
|
99 |
-
LOW_PRICE = 5
|
100 |
-
CUSTOM = "custom(비싼모델전용)"
|
101 |
-
EXAMPLE_SPRING_URLS = [
|
102 |
-
"static/examples/out-1.webp",
|
103 |
-
"static/examples/out-2.webp",
|
104 |
-
"static/examples/out-3.webp",
|
105 |
-
"static/examples/out-4.webp",
|
106 |
-
"static/examples/out-5.webp",
|
107 |
-
"static/examples/out-6.webp",
|
108 |
-
"static/examples/out-7.webp",
|
109 |
-
"static/examples/out-0.webp",
|
110 |
-
]
|
111 |
-
|
112 |
-
EXAMPLE_BEAUTY_URLS = [
|
113 |
-
"static/beauty/out-1.webp",
|
114 |
-
"static/beauty/out-2.webp",
|
115 |
-
"static/beauty/out-3.webp",
|
116 |
-
"static/beauty/out-4.webp",
|
117 |
-
"static/beauty/out-5.webp",
|
118 |
-
"static/beauty/out-6.webp",
|
119 |
-
"static/beauty/out-7.webp",
|
120 |
-
"static/beauty/out-0.webp",
|
121 |
-
]
|
122 |
-
|
123 |
-
EXAMPLE_ENTRY_URLS = []
|
124 |
-
# EXAMPLE_ENTRY_URLS = EXAMPLE_BEAUTY_URLS + EXAMPLE_SPRING_URLS
|
125 |
-
# shuffle(EXAMPLE_ENTRY_URLS)
|
126 |
-
|
127 |
-
examplePrompt = "A breathtaking beautiful Korean woman."
|
128 |
-
|
129 |
-
|
130 |
-
def genFluxPrompt(userPrompt, additionalComment, ratio, imageStyle):
|
131 |
-
"""
|
132 |
-
userPrompt 와 additionalComment 를 받아서 비율에 맞는 이미지 생성을 위한 prompt 생성.
|
133 |
-
"""
|
134 |
-
imageStyle = imageStyleMap[imageStyle]
|
135 |
-
# print("genFluxPrompt ", userPrompt, ratio, imageStyle)
|
136 |
-
# The ibm-granite/granite-3.1-8b-instruct model can stream output as it's running.
|
137 |
-
|
138 |
-
additionalComment = (
|
139 |
-
f"\n# Important note: {additionalComment}\n" if additionalComment else ""
|
140 |
-
)
|
141 |
-
|
142 |
-
prompt = (
|
143 |
-
f"Please create a prompt that will generate a best quality image that can express the following sentence in English.(DO NOT INCLUDE ANY KOREAN LANGUAGE.)"
|
144 |
-
f"\n# Image generation info\n"
|
145 |
-
f"\n- Aspect ratio: {ratio}\n"
|
146 |
-
f"\n- Image style: {imageStyle}\n"
|
147 |
-
"\n# Description for the image.\n" + userPrompt + additionalComment
|
148 |
-
)
|
149 |
-
|
150 |
-
print("genFluxPrompt prompt:", prompt)
|
151 |
-
|
152 |
-
result = replicate.run(
|
153 |
-
"anthropic/claude-3.5-haiku",
|
154 |
-
input={
|
155 |
-
"top_k": 50,
|
156 |
-
"top_p": 0.9,
|
157 |
-
"prompt": prompt,
|
158 |
-
"max_tokens": 256,
|
159 |
-
"min_tokens": 0,
|
160 |
-
"temperature": 0.6,
|
161 |
-
"system_prompt": (
|
162 |
-
"You are a professional prompt engineer specializing in creating prompts for txt2img AI model."
|
163 |
-
"You always create prompts that can extract the impressive output."
|
164 |
-
"Make sure to emphasize the intention for 'Important note:' section in the prompts if it exists."
|
165 |
-
"Do not generate negative prompts!"
|
166 |
-
),
|
167 |
-
"presence_penalty": 0,
|
168 |
-
"frequency_penalty": 0,
|
169 |
-
},
|
170 |
-
)
|
171 |
-
prompt = to_string(result)
|
172 |
-
print("genFluxPrompt prompt:", prompt)
|
173 |
-
return prompt
|
174 |
-
|
175 |
-
|
176 |
-
imageModelMap = {
|
177 |
-
HIGH_PRICE_OPTION: "black-forest-labs/flux-1.1-pro",
|
178 |
-
LOW_PRICE_OPTION: "black-forest-labs/flux-schnell",
|
179 |
-
}
|
180 |
-
|
181 |
-
|
182 |
-
def genImage(prompt, ratio, imageModelKind, imgWidth, imgHeight, imageNum=1):
|
183 |
-
"""
|
184 |
-
prompt 를 받아서 비율에 맞는 이미지 imageNum 만큼 생성.
|
185 |
-
"""
|
186 |
-
|
187 |
-
imageModel = imageModelMap[imageModelKind]
|
188 |
-
|
189 |
-
if ratio == CUSTOM:
|
190 |
-
input = {
|
191 |
-
"prompt": prompt,
|
192 |
-
"aspect_ratio": ratio, # gradio radio 버튼에서 선택된 값
|
193 |
-
"width": int(imgWidth),
|
194 |
-
"height": int(imgHeight),
|
195 |
-
"aspect_ratio": "custom",
|
196 |
-
"output_format": "webp",
|
197 |
-
"output_quality": 90,
|
198 |
-
}
|
199 |
-
# custom 일 때는 비싼 모델로 생성.
|
200 |
-
imageModel = imageModelMap[HIGH_PRICE_OPTION]
|
201 |
-
else:
|
202 |
-
input = {
|
203 |
-
"prompt": prompt,
|
204 |
-
"aspect_ratio": ratio, # gradio radio 버튼에서 선택된 값
|
205 |
-
"output_format": "webp",
|
206 |
-
"output_quality": 90,
|
207 |
-
"num_outputs": imageNum,
|
208 |
-
}
|
209 |
-
|
210 |
-
print("genImage input:", input)
|
211 |
-
|
212 |
-
imageModel = imageModelMap[imageModelKind]
|
213 |
-
result = replicate.run(imageModel, input=input)
|
214 |
-
|
215 |
-
print("Generated Image Info:", result)
|
216 |
-
# imgUrls = [str(img) for img in result]
|
217 |
-
# imgUrl = str(result) if type(result) != list else str(result[0])
|
218 |
-
imgUrl = str(result) if not isinstance(result, list) else str(result[0])
|
219 |
-
if imageNum > 1:
|
220 |
-
imgUrls = [str(img) for img in result]
|
221 |
-
print("genImage imgUrls:", imgUrls)
|
222 |
-
return imgUrls
|
223 |
-
else:
|
224 |
-
print("genImage imgUrl:", imgUrl)
|
225 |
-
return imgUrl
|
226 |
-
|
227 |
-
|
228 |
-
def reduxImage(prompt, url):
|
229 |
-
"""
|
230 |
-
url 의 이미지를 재생성.
|
231 |
-
|
232 |
-
output = replicate.run(
|
233 |
-
"black-forest-labs/flux-1.1-pro",
|
234 |
-
input={
|
235 |
-
"width": 1024,
|
236 |
-
"height": 480,
|
237 |
-
"prompt": "one boy and 3 girls hands and hands playing in the school.",
|
238 |
-
"aspect_ratio": "custom",
|
239 |
-
"image_prompt": "https://replicate.delivery/xezq/CLKVkX1QXeXrMy1tH43zJmCk6RgBszqmQdK45AOUTedCkyUUA/tmpucuderjn.webp",
|
240 |
-
"output_format": "webp",
|
241 |
-
"output_quality": 80,
|
242 |
-
"safety_tolerance": 2,
|
243 |
-
"prompt_upsampling": True
|
244 |
-
}
|
245 |
-
)
|
246 |
-
"""
|
247 |
-
result = replicate.run(
|
248 |
-
"black-forest-labs/flux-1.1-pro",
|
249 |
-
input={
|
250 |
-
"prompt": prompt,
|
251 |
-
"aspect_ratio": "16:9",
|
252 |
-
"image_prompt": url,
|
253 |
-
"output_format": "webp",
|
254 |
-
"output_quality": 80,
|
255 |
-
"safety_tolerance": 2, # 1 is most strict and 6 is most permissive
|
256 |
-
"prompt_upsampling": True,
|
257 |
-
},
|
258 |
-
)
|
259 |
-
return str(result)
|
260 |
-
|
261 |
-
|
262 |
-
def genPromptAndImage(
|
263 |
-
userPrompt,
|
264 |
-
additionalComment,
|
265 |
-
fluxPrompt,
|
266 |
-
ratio,
|
267 |
-
imageStyle,
|
268 |
-
imageModel,
|
269 |
-
highImageCnt,
|
270 |
-
lowImageCnt,
|
271 |
-
imgWidth,
|
272 |
-
imgHeight,
|
273 |
-
):
|
274 |
-
"""
|
275 |
-
fluxPrompt 가 빈 문자열이면 새로운 prompt 를 생성하고,
|
276 |
-
빈 문자열이 아니면 기존의 prompt 를 사용해서 이미지 생성.
|
277 |
-
"""
|
278 |
-
print(
|
279 |
-
"genPromptAndImage:",
|
280 |
-
ratio,
|
281 |
-
imageStyle,
|
282 |
-
imageModel,
|
283 |
-
highImageCnt,
|
284 |
-
lowImageCnt,
|
285 |
-
)
|
286 |
-
|
287 |
-
if fluxPrompt == "":
|
288 |
-
fluxPrompt = genFluxPrompt(userPrompt, additionalComment, ratio, imageStyle)
|
289 |
-
|
290 |
-
imgUrl = genImage(fluxPrompt, ratio, imageModel, imgWidth, imgHeight)
|
291 |
-
|
292 |
-
highImageCnt = int(highImageCnt)
|
293 |
-
lowImageCnt = int(lowImageCnt)
|
294 |
-
if imageModel == HIGH_PRICE_OPTION:
|
295 |
-
highImageCnt += 1
|
296 |
-
else:
|
297 |
-
lowImageCnt += 1
|
298 |
-
|
299 |
-
totalPrice = highImageCnt * HIGH_PRICE + lowImageCnt * LOW_PRICE
|
300 |
-
return fluxPrompt, highImageCnt, lowImageCnt, totalPrice, imgUrl, None
|
301 |
-
|
302 |
-
|
303 |
-
# def genImageWithOrgText(
|
304 |
-
# userPrompt, additionalComment, fluxPrompt, ratio, imageStyle, imageModel, highImageCnt, lowImageCnt
|
305 |
-
# ):
|
306 |
-
# """
|
307 |
-
# userPrompt와 additionalComment 만으로 직접 image 를 생성.
|
308 |
-
# """
|
309 |
-
# fluxPrompt = (
|
310 |
-
# userPrompt
|
311 |
-
# + "\n\nImportant information: "
|
312 |
-
# + additionalComment
|
313 |
-
# + "\n\nImage style: "
|
314 |
-
# + imageStyle
|
315 |
-
# )
|
316 |
-
# return genPromptAndImage(userPrompt, additionalComment, fluxPrompt, ratio, imageStyle)
|
317 |
-
|
318 |
-
|
319 |
-
APP_TITLE = """
|
320 |
-
<div style="display: flex; align-items: center; justify-content: space-between; width: 100%">
|
321 |
-
<div style="display: flex; align-items: center; gap: 10px">
|
322 |
-
<picture>
|
323 |
-
<source srcset="/gradio_api/file=static/image/daekyo_logo_kr_white.png" media="(prefers-color-scheme: dark)">
|
324 |
-
<img src="/gradio_api/file=static/image/daekyo_logo_kr.png" width='70' height='70'/>
|
325 |
-
</picture>
|
326 |
-
<span style="font-size: 40px; font-weight: bold; margin-left: 10px;"> 대교 AI 콘텐츠 이미지 월드컵 🏆</span>
|
327 |
-
</div>
|
328 |
-
<a href="/logout" style="text-decoration: none;">
|
329 |
-
<button style="padding: 8px 16px; font-size: 16px; background-color: #446699; color: white; border: none; border-radius: 4px; cursor: pointer;">Logout</button>
|
330 |
-
</a>
|
331 |
-
</div>
|
332 |
-
"""
|
333 |
-
|
334 |
-
GEN_IMAGE_DESC = """봄을 담은 정원
|
335 |
-
It was the start of spring.
|
336 |
-
Our teacher had a great idea.
|
337 |
-
"Let's make a vegetable garden behind the school!"
|
338 |
-
We all worked together.
|
339 |
-
First, we dug holes.
|
340 |
-
Next, we planted different seeds.
|
341 |
-
After that, we watered the garden every day.
|
342 |
-
Tiny plants soon appeared in the soil.
|
343 |
-
Slowly, they got bigger and bigger.
|
344 |
-
Finally, we picked the vegetables!
|
345 |
-
There were tomatoes, lettuce, and carrots.
|
346 |
-
We cut them into small pieces and made a colorful salad!
|
347 |
-
It was fresh and delicious. """
|
348 |
-
|
349 |
-
CSS = """
|
350 |
-
footer {visibility: hidden}
|
351 |
-
|
352 |
-
/* 이미지 클릭할 때, 포인터 커서로 변경 */
|
353 |
-
.battle-image img { cursor: pointer !important; }
|
354 |
-
|
355 |
-
.tiny-input {
|
356 |
-
width: '20px',
|
357 |
-
}
|
358 |
-
|
359 |
-
/***********************************************/
|
360 |
-
/* 이미지 사라지는 애니메이션 */
|
361 |
-
/***********************************************/
|
362 |
-
/* CSS for fade-out animation */
|
363 |
-
.fade-out {
|
364 |
-
animation: fadeOutAndShrink 1s forwards;
|
365 |
-
/* Add these properties to ensure smooth animation */
|
366 |
-
transform-origin: center;
|
367 |
-
display: inline-block;
|
368 |
-
}
|
369 |
-
|
370 |
-
@keyframes fadeOutAndShrink {
|
371 |
-
0% {
|
372 |
-
opacity: 1;
|
373 |
-
transform: scale(1);
|
374 |
-
}
|
375 |
-
100% {
|
376 |
-
opacity: 0;
|
377 |
-
transform: scale(0);
|
378 |
-
}
|
379 |
-
}
|
380 |
-
|
381 |
-
/* Optional: Add this if you want to keep the layout space after element disappears */
|
382 |
-
.fade-out.preserve-space {
|
383 |
-
visibility: hidden;
|
384 |
-
opacity: 0;
|
385 |
-
animation: fadeOutAndShrinkPreserve 1s forwards;
|
386 |
-
}
|
387 |
-
|
388 |
-
@keyframes fadeOutAndShrinkPreserve {
|
389 |
-
0% {
|
390 |
-
opacity: 1;
|
391 |
-
transform: scale(1);
|
392 |
-
visibility: visible;
|
393 |
-
}
|
394 |
-
99% {
|
395 |
-
transform: scale(0.01);
|
396 |
-
opacity: 0;
|
397 |
-
visibility: visible;
|
398 |
-
}
|
399 |
-
100% {
|
400 |
-
transform: scale(0);
|
401 |
-
opacity: 0;
|
402 |
-
visibility: hidden;
|
403 |
-
}
|
404 |
-
}
|
405 |
-
|
406 |
-
|
407 |
-
/***********************************************/
|
408 |
-
/* 이미지 강조를 위해 키우는 애니메이션 */
|
409 |
-
/***********************************************/
|
410 |
-
/* CSS for fade-in animation with scale */
|
411 |
-
.fade-in {
|
412 |
-
animation: fadeInAndScale 0.3s ease forwards;
|
413 |
-
/* Add these properties to ensure smooth animation */
|
414 |
-
transform-origin: center;
|
415 |
-
display: inline-block;
|
416 |
-
opacity: 0; /* Start with opacity 0 */
|
417 |
-
}
|
418 |
-
|
419 |
-
@keyframes fadeInAndScale {
|
420 |
-
0% {
|
421 |
-
opacity: 0;
|
422 |
-
transform: scale(1);
|
423 |
-
}
|
424 |
-
100% {
|
425 |
-
opacity: 1;
|
426 |
-
transform: scale(1.15); /* Element grows to 1.15 times its original size */
|
427 |
-
}
|
428 |
-
}
|
429 |
-
|
430 |
-
|
431 |
-
/***********************************************/
|
432 |
-
/* 처음에는 안보이다가 점점 나타나는 애니메이션 */
|
433 |
-
/***********************************************/
|
434 |
-
.fade-in-visibility {
|
435 |
-
/* 처음에는 보이지 않음 */
|
436 |
-
opacity: 0;
|
437 |
-
/* 애니메이션 설정 */
|
438 |
-
animation: fadeInVisibility 1s ease-in forwards;
|
439 |
-
/* 애니메이션을 부드럽게 만들기 위한 설정 */
|
440 |
-
will-change: opacity;
|
441 |
-
}
|
442 |
-
|
443 |
-
@keyframes fadeInVisibility {
|
444 |
-
0% {
|
445 |
-
opacity: 0;
|
446 |
-
}
|
447 |
-
100% {
|
448 |
-
opacity: 1;
|
449 |
-
}
|
450 |
-
}
|
451 |
-
|
452 |
-
/***********************************************/
|
453 |
-
/* 이미지 클릭 비활성화 */
|
454 |
-
/***********************************************/
|
455 |
-
.non-clickable {
|
456 |
-
pointer-events: none; /* 모든 포인터 이벤트(클릭, 호버 등)를 비활성화 */
|
457 |
-
user-select: none; /* 텍스트 선택 방지 */
|
458 |
-
}
|
459 |
"""
|
|
|
460 |
|
461 |
-
|
|
|
|
|
462 |
|
463 |
-
|
|
|
|
|
|
|
464 |
|
465 |
-
|
466 |
-
# theme = "JohnSmith9982/small_and_pretty" # 연두색 테마
|
467 |
-
# theme = "ParityError/Interstellar" # 보라색 테마
|
468 |
-
# theme = gr.themes.Ocean() # 연두
|
469 |
-
# theme = gr.themes.Origin() # Default 주황
|
470 |
-
theme = gr.themes.Base() # 파랑 테마 심플.
|
471 |
|
472 |
|
473 |
def setup_demo():
|
|
|
|
|
|
|
474 |
with gr.Blocks(theme=theme, css=CSS) as demo:
|
475 |
-
#
|
476 |
-
# gr.Image(value="static/examples/out-4.webp")
|
477 |
-
current_img_url = gr.State("")
|
478 |
-
selectedImageIndex = gr.State(0)
|
479 |
-
entryImageUrls = gr.State(EXAMPLE_ENTRY_URLS) # 저장된 URL 목록
|
480 |
-
# entryImageUrls = gr.State([]) # 저장된 URL 목록
|
481 |
-
|
482 |
-
# gr.Markdown("#  대교 콘텐츠 이미지 생성기")
|
483 |
gr.Markdown(APP_TITLE)
|
484 |
-
with gr.Tab("이미지 생성"):
|
485 |
-
with gr.Column(): # Column을 사용하여 수직 배열
|
486 |
-
# with gr.Group():
|
487 |
-
with gr.Row():
|
488 |
-
# 주: Hot reload 한 후 logout을 하면 "AttributeError: 'Blocks' object has no attribute 'auth_message'" 라는 메시지가 뜨면서 gradio 서버가 죽음.
|
489 |
-
# logoutBtn = gr.Button("Logout", link="/logout")
|
490 |
-
with gr.Column(): # Column을 사용하여 수직 배열
|
491 |
-
userPrompt = gr.TextArea(
|
492 |
-
label="교재 지문 (필수)",
|
493 |
-
value=GEN_IMAGE_DESC,
|
494 |
-
max_lines=5,
|
495 |
-
)
|
496 |
-
additionalComment = gr.Textbox(
|
497 |
-
label="추가 희망사항 (옵셔널)", lines=2, value=""
|
498 |
-
)
|
499 |
-
with gr.Column():
|
500 |
-
with gr.Accordion("상세 설정", open=False):
|
501 |
-
with gr.Column(): # Column을 사용하여 수직 배열
|
502 |
-
with gr.Row():
|
503 |
-
highImgCnt = gr.Textbox(
|
504 |
-
label="비싼 이미지",
|
505 |
-
value="0",
|
506 |
-
interactive=False,
|
507 |
-
)
|
508 |
-
lowImgCnt = gr.Textbox(
|
509 |
-
label="싼 이미지", value="0", interactive=False
|
510 |
-
)
|
511 |
-
totalPrice = gr.Textbox(
|
512 |
-
label="총 이미지 생성 비용(원)",
|
513 |
-
value="0",
|
514 |
-
interactive=False,
|
515 |
-
)
|
516 |
-
genModelKind = gr.Radio(
|
517 |
-
[LOW_PRICE_OPTION, HIGH_PRICE_OPTION],
|
518 |
-
label="이미지 생성 모델",
|
519 |
-
value=LOW_PRICE_OPTION,
|
520 |
-
)
|
521 |
-
with gr.Row():
|
522 |
-
aspectRatio = gr.Radio(
|
523 |
-
["1:1", "4:3", "16:9", CUSTOM],
|
524 |
-
label="이미지 비율",
|
525 |
-
value="16:9",
|
526 |
-
scale=4,
|
527 |
-
)
|
528 |
-
imgWidth = gr.Number(
|
529 |
-
label="너비 (32의배수)",
|
530 |
-
value=1440,
|
531 |
-
scale=1,
|
532 |
-
visible=False,
|
533 |
-
maximum=1440,
|
534 |
-
minimum=256,
|
535 |
-
step=32,
|
536 |
-
)
|
537 |
-
imgHeight = gr.Number(
|
538 |
-
label="높이 (32의배수)",
|
539 |
-
value=768,
|
540 |
-
scale=1,
|
541 |
-
visible=False,
|
542 |
-
maximum=1440,
|
543 |
-
minimum=256,
|
544 |
-
step=32,
|
545 |
-
)
|
546 |
-
|
547 |
-
def custom_aspect_ratio(ratio):
|
548 |
-
if ratio == CUSTOM:
|
549 |
-
return (
|
550 |
-
# HIGH_PRICE_OPTION,
|
551 |
-
gr.update(
|
552 |
-
value=HIGH_PRICE_OPTION,
|
553 |
-
interactive=False,
|
554 |
-
),
|
555 |
-
gr.update(visible=True),
|
556 |
-
gr.update(visible=True),
|
557 |
-
)
|
558 |
-
else:
|
559 |
-
return (
|
560 |
-
gr.update(interactive=True),
|
561 |
-
gr.update(visible=False),
|
562 |
-
gr.update(visible=False),
|
563 |
-
)
|
564 |
-
|
565 |
-
# genModelKind HIGH_PRICE_OPTION 으로 바꿔주는 곳은 아래서 해주고 있음.
|
566 |
-
aspectRatio.change(
|
567 |
-
fn=custom_aspect_ratio,
|
568 |
-
inputs=aspectRatio,
|
569 |
-
outputs=[genModelKind, imgWidth, imgHeight],
|
570 |
-
)
|
571 |
-
|
572 |
-
styleList = list(imageStyleMap.keys())
|
573 |
-
imageStyle = gr.Radio(
|
574 |
-
styleList, label="이미지 스타일", value="알아서"
|
575 |
-
)
|
576 |
-
with gr.Accordion("Prompt info", open=False):
|
577 |
-
fluxPrompt = gr.Textbox(
|
578 |
-
label="Flux Prompt", value="", lines=10
|
579 |
-
)
|
580 |
-
|
581 |
-
with gr.Row():
|
582 |
-
genSmartImage = gr.Button("교재 지문 삽화 이미지 생성!")
|
583 |
-
removeEntryWorldcupBtn = gr.Button("현재 이미지 제거")
|
584 |
-
|
585 |
-
with gr.Column():
|
586 |
-
# entryList = gr.State([])
|
587 |
-
# outputImg = gr.Image(label="생성된 이미지")
|
588 |
-
progressBar = gr.Image(
|
589 |
-
label=None, interactive=False, visible=False, height=28
|
590 |
-
)
|
591 |
-
entryListGallery = gr.Gallery(
|
592 |
-
label="이미지 월드컵 진출 이미지",
|
593 |
-
preview=True,
|
594 |
-
allow_preview=True,
|
595 |
-
interactive=False,
|
596 |
-
# height=768,
|
597 |
-
)
|
598 |
-
with gr.Row():
|
599 |
-
entryCount = gr.Markdown("## 이미지 월드컵 진출 이미지 수: 0")
|
600 |
-
goImageWorldCupBtn = gr.Button(
|
601 |
-
"이미지 월드컵 하러가기", visible=False
|
602 |
-
)
|
603 |
-
|
604 |
-
@entryListGallery.select(outputs=selectedImageIndex)
|
605 |
-
def setSelectedImageIndex(selection: gr.SelectData):
|
606 |
-
return selection.index
|
607 |
-
|
608 |
-
def removeSelectedEntry(entryList, selectedIndex):
|
609 |
-
if selectedIndex is None or len(entryList) == 0:
|
610 |
-
return gr.update(value=entryList), entryList
|
611 |
-
|
612 |
-
del entryList[selectedIndex]
|
613 |
-
return (
|
614 |
-
entryList,
|
615 |
-
entryList,
|
616 |
-
f"## 이미지 월드컵 진출 이미지 수: {len(entryList)}",
|
617 |
-
)
|
618 |
-
|
619 |
-
# 왠지 모르게 마지막 요소를 지우면 preview=True 가 안되서 이렇게 함.
|
620 |
-
def reSelectedEntry(entryList):
|
621 |
-
return gr.update(selected_index=len(entryList) - 1, preview=True)
|
622 |
-
|
623 |
-
removeEntryWorldcupBtn.click(
|
624 |
-
fn=removeSelectedEntry,
|
625 |
-
inputs=[entryImageUrls, selectedImageIndex],
|
626 |
-
outputs=[entryListGallery, entryImageUrls, entryCount],
|
627 |
-
).then(
|
628 |
-
fn=reSelectedEntry,
|
629 |
-
inputs=entryImageUrls,
|
630 |
-
outputs=entryListGallery,
|
631 |
-
)
|
632 |
-
|
633 |
-
# 이벤트 처리.
|
634 |
-
# 입력값이 변경될 때마다 Flux Prompt 초기화
|
635 |
-
for component in [
|
636 |
-
userPrompt,
|
637 |
-
aspectRatio,
|
638 |
-
imageStyle,
|
639 |
-
additionalComment,
|
640 |
-
]:
|
641 |
-
component.change(
|
642 |
-
fn=lambda x: "", inputs=fluxPrompt, outputs=fluxPrompt
|
643 |
-
)
|
644 |
-
genImageInputs = [
|
645 |
-
userPrompt,
|
646 |
-
additionalComment,
|
647 |
-
fluxPrompt,
|
648 |
-
aspectRatio,
|
649 |
-
imageStyle,
|
650 |
-
genModelKind,
|
651 |
-
highImgCnt,
|
652 |
-
lowImgCnt,
|
653 |
-
imgWidth,
|
654 |
-
imgHeight,
|
655 |
-
]
|
656 |
|
657 |
-
|
658 |
-
|
659 |
|
660 |
-
|
661 |
-
|
662 |
-
|
663 |
-
genSmartImage.click(fn=show_progress, outputs=progressBar).then(
|
664 |
-
fn=genPromptAndImage,
|
665 |
-
inputs=genImageInputs,
|
666 |
-
outputs=[
|
667 |
-
fluxPrompt,
|
668 |
-
highImgCnt,
|
669 |
-
lowImgCnt,
|
670 |
-
totalPrice,
|
671 |
-
current_img_url,
|
672 |
-
progressBar,
|
673 |
-
],
|
674 |
-
).then(
|
675 |
-
fn=lambda entryList, imgUrl: (
|
676 |
-
gr.update(
|
677 |
-
value=[*entryList, imgUrl],
|
678 |
-
preview=True,
|
679 |
-
selected_index=len(entryList),
|
680 |
-
),
|
681 |
-
entryList + [imgUrl],
|
682 |
-
len(entryList),
|
683 |
-
gr.update(visible=False),
|
684 |
-
),
|
685 |
-
inputs=[entryImageUrls, current_img_url],
|
686 |
-
outputs=[
|
687 |
-
entryListGallery,
|
688 |
-
entryImageUrls,
|
689 |
-
selectedImageIndex,
|
690 |
-
progressBar,
|
691 |
-
],
|
692 |
-
).then(
|
693 |
-
fn=lambda entryImageUrls: f"## 이미지 월드컵 진출 이미지 수: {len(entryImageUrls)}",
|
694 |
-
inputs=entryImageUrls,
|
695 |
-
outputs=entryCount,
|
696 |
-
).then(
|
697 |
-
fn=lambda entryCountTxt, entryList: (
|
698 |
-
[
|
699 |
-
entryCountTxt + " (이미지 월드컵 가능!)",
|
700 |
-
gr.update(visible=False),
|
701 |
-
gr.Info(
|
702 |
-
f"{len(entryList)}개의 이미지가 만들어졌습니다. '이미지 월드컵'을 시작하실 수 있습니다!",
|
703 |
-
duration=5,
|
704 |
-
),
|
705 |
-
]
|
706 |
-
if len(entryList) in WORLDCUPABLE_IMAGE_CNT
|
707 |
-
else [entryCountTxt, gr.update(visible=False)]
|
708 |
-
),
|
709 |
-
inputs=[entryCount, entryImageUrls],
|
710 |
-
outputs=[entryCount, goImageWorldCupBtn],
|
711 |
-
)
|
712 |
-
|
713 |
-
with gr.Tab("이미지 월드컵"):
|
714 |
-
# vsIndex = gr.State(0)
|
715 |
-
# worldcupTitle = gr.Markdown("# 이미지 월드컵")
|
716 |
-
worldcup = gr.State()
|
717 |
-
with gr.Column(): # Column을 사용하여 수직 배열
|
718 |
-
startWorldcupBtn = gr.Button("월드컵 시작!")
|
719 |
-
|
720 |
-
with gr.Row():
|
721 |
-
winImage1Btn = gr.Button("이미지 1 승리!", scale=5, visible=False)
|
722 |
-
# worldcupTitle = gr.HTML("<span> </span>")
|
723 |
-
worldcupTitle = gr.Markdown("# ")
|
724 |
-
winImage2Btn = gr.Button("이미지 2 승리!", scale=5, visible=False)
|
725 |
-
|
726 |
-
with gr.Row(): # 이미지 수평 배열.
|
727 |
-
battleImage1 = gr.Image(
|
728 |
-
show_label=False,
|
729 |
-
scale=5,
|
730 |
-
elem_classes="battle-image",
|
731 |
-
interactive=False,
|
732 |
-
show_download_button=False,
|
733 |
-
)
|
734 |
-
vsImage = gr.HTML(VS_IMAGE)
|
735 |
-
battleImage2 = gr.Image(
|
736 |
-
show_label=False,
|
737 |
-
scale=5,
|
738 |
-
elem_classes="battle-image",
|
739 |
-
interactive=False,
|
740 |
-
show_download_button=False,
|
741 |
-
)
|
742 |
-
|
743 |
-
def winImage0(winnerIndex):
|
744 |
-
print("winImage0 winnerIndex:", winnerIndex)
|
745 |
-
if winnerIndex == 0:
|
746 |
-
return gr.update(
|
747 |
-
elem_classes="fade-in non-clickable"
|
748 |
-
), gr.update(elem_classes="fade-out")
|
749 |
-
else:
|
750 |
-
return gr.update(elem_classes="fade-out"), gr.update(
|
751 |
-
elem_classes="fade-in non-clickable"
|
752 |
-
)
|
753 |
-
|
754 |
-
def sleep():
|
755 |
-
time.sleep(1)
|
756 |
-
|
757 |
-
def winImage(winnerIndex, worldcup):
|
758 |
-
print("winnerIndex:", winnerIndex)
|
759 |
-
print("worldcup:", worldcup)
|
760 |
-
finalWinner = worldcup.winImage(winnerIndex)
|
761 |
-
battleTitle = worldcup.getKangRound()
|
762 |
-
if finalWinner:
|
763 |
-
return (
|
764 |
-
gr.update(
|
765 |
-
visible=winnerIndex == 0,
|
766 |
-
elem_classes=BATTLE_IMAGE_INIT,
|
767 |
-
),
|
768 |
-
gr.update(
|
769 |
-
visible=winnerIndex == 1,
|
770 |
-
elem_classes=BATTLE_IMAGE_INIT,
|
771 |
-
),
|
772 |
-
worldcup,
|
773 |
-
"## <center>🎉 우승 🎊</center>",
|
774 |
-
gr.update(visible=False),
|
775 |
-
gr.update(visible=True),
|
776 |
-
gr.update(visible=False),
|
777 |
-
gr.update(visible=False),
|
778 |
-
)
|
779 |
-
nextMatchImages = worldcup.getCurrentRoundImages()
|
780 |
-
print("nextMatchImages:", nextMatchImages)
|
781 |
-
return (
|
782 |
-
gr.update(
|
783 |
-
value=nextMatchImages[0],
|
784 |
-
elem_classes=BATTLE_IMAGE_INIT,
|
785 |
-
),
|
786 |
-
gr.update(
|
787 |
-
value=nextMatchImages[1],
|
788 |
-
elem_classes=BATTLE_IMAGE_INIT,
|
789 |
-
),
|
790 |
-
worldcup,
|
791 |
-
battleTitle,
|
792 |
-
gr.skip(),
|
793 |
-
gr.skip(),
|
794 |
-
gr.skip(),
|
795 |
-
gr.skip(),
|
796 |
-
)
|
797 |
-
|
798 |
-
wOutputs = [
|
799 |
-
battleImage1,
|
800 |
-
battleImage2,
|
801 |
-
worldcup,
|
802 |
-
worldcupTitle,
|
803 |
-
vsImage,
|
804 |
-
startWorldcupBtn,
|
805 |
-
winImage1Btn,
|
806 |
-
winImage2Btn,
|
807 |
-
]
|
808 |
-
|
809 |
-
# winImage1Btn_click = winImage1Btn.click
|
810 |
-
# winImage1Btn_click(
|
811 |
-
# fn=lambda w: winImage0(0),
|
812 |
-
# inputs=[battleImage1],
|
813 |
-
# outputs=[battleImage1, battleImage2],
|
814 |
-
# ).then(fn=sleep, inputs=[], outputs=[]).then(
|
815 |
-
# fn=lambda w: winImage(0, w), inputs=[worldcup], outputs=wOutputs
|
816 |
-
# )
|
817 |
-
# winImage2Btn.click(
|
818 |
-
# fn=lambda w: winImage(1, w), inputs=[worldcup], outputs=wOutputs
|
819 |
-
# )
|
820 |
-
# battleImage1.select(
|
821 |
-
# fn=lambda w: winImage(0, w), inputs=[worldcup], outputs=wOutputs
|
822 |
-
# )
|
823 |
-
# battleImage2.select(
|
824 |
-
# fn=lambda w: winImage(1, w), inputs=[worldcup], outputs=wOutputs
|
825 |
-
# )
|
826 |
-
|
827 |
-
for event, winIndex in [
|
828 |
-
(winImage1Btn.click, 0),
|
829 |
-
(winImage2Btn.click, 1),
|
830 |
-
(battleImage1.select, 0),
|
831 |
-
(battleImage2.select, 1),
|
832 |
-
]:
|
833 |
-
event(
|
834 |
-
fn=lambda w, i=winIndex: winImage0(i),
|
835 |
-
inputs=[battleImage1],
|
836 |
-
outputs=[battleImage1, battleImage2],
|
837 |
-
).then(fn=sleep).then(
|
838 |
-
fn=lambda w, i=winIndex: winImage(i, w),
|
839 |
-
inputs=[worldcup],
|
840 |
-
outputs=wOutputs,
|
841 |
-
)
|
842 |
-
|
843 |
-
@startWorldcupBtn.click(
|
844 |
-
inputs=[entryImageUrls],
|
845 |
-
outputs=[
|
846 |
-
worldcup,
|
847 |
-
battleImage1,
|
848 |
-
battleImage2,
|
849 |
-
startWorldcupBtn,
|
850 |
-
winImage1Btn,
|
851 |
-
winImage2Btn,
|
852 |
-
vsImage,
|
853 |
-
worldcupTitle,
|
854 |
-
],
|
855 |
-
)
|
856 |
-
def startWorldcupBtn(entryImageUrls):
|
857 |
-
print("startWorldcupBtn entryImageUrls:", entryImageUrls)
|
858 |
-
if len(entryImageUrls) in WORLDCUPABLE_IMAGE_CNT:
|
859 |
-
worldcup = Worldcup(images=entryImageUrls)
|
860 |
-
twoImages = worldcup.getCurrentRoundImages()
|
861 |
-
return (
|
862 |
-
worldcup,
|
863 |
-
gr.update(
|
864 |
-
value=twoImages[0],
|
865 |
-
visible=True,
|
866 |
-
elem_classes=BATTLE_IMAGE_INIT,
|
867 |
-
),
|
868 |
-
gr.update(
|
869 |
-
value=twoImages[1],
|
870 |
-
visible=True,
|
871 |
-
elem_classes=BATTLE_IMAGE_INIT,
|
872 |
-
),
|
873 |
-
gr.update(visible=False),
|
874 |
-
gr.update(visible=True),
|
875 |
-
gr.update(visible=True),
|
876 |
-
gr.update(visible=True),
|
877 |
-
worldcup.getKangRound(),
|
878 |
-
)
|
879 |
-
else:
|
880 |
-
gr.Warning(
|
881 |
-
"월드컵 참가 이미지 수는 4개, 8개 또는 16개여야 합니다.",
|
882 |
-
duration=5,
|
883 |
-
)
|
884 |
-
return gr.skip()
|
885 |
-
|
886 |
-
# load_example_btn = gr.Button("예제 이미지 4개 생성 (20원)")
|
887 |
-
|
888 |
-
# @load_example_btn.click(inputs=genImageInputs, outputs=entryImageUrls)
|
889 |
-
# def load_example_images(*args):
|
890 |
-
# gr.Warning("예제 이미지 로드중...", duration=5)
|
891 |
-
# imgUrls = genImage(
|
892 |
-
# prompt=examplePrompt,
|
893 |
-
# ratio="4:3",
|
894 |
-
# imageModelKind=LOW_PRICE_OPTION,
|
895 |
-
# imgWidth=1024,
|
896 |
-
# imgHeight=768,
|
897 |
-
# imageNum=4,
|
898 |
-
# )
|
899 |
-
# gr.Info(
|
900 |
-
# "예제 이미지가 로드되었습니다. '월드컵 시작!' 버튼을 눌러주세요.",
|
901 |
-
# duration=5,
|
902 |
-
# )
|
903 |
-
# return imgUrls
|
904 |
|
905 |
return demo
|
906 |
|
907 |
|
908 |
-
#
|
909 |
demo = setup_demo()
|
910 |
|
911 |
-
# print("os.environ.get(getenv('default_pw')):", os.environ.get("default_pw"))
|
912 |
-
|
913 |
if __name__ == "__main__":
|
914 |
demo.launch(
|
915 |
auth=("daekyo", os.environ.get("DEFAULT_PW")),
|
916 |
server_name="0.0.0.0",
|
917 |
ssr_mode=False,
|
918 |
)
|
919 |
-
|
920 |
-
# TODO:
|
921 |
-
# - 이미지 사이즈를 원하는대로 변경하는 기능. (flux 1.1 pro 에서 지원)
|
922 |
-
# - 이미지를 제시해주고 그 이미지와 비슷한 이미지를 만드는 기능. (img2img)
|
923 |
-
# - 특정 부분만 수정하는 기능. https://replicate.com/black-forest-labs/flux-fill-pro
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
"""
|
2 |
+
대교 AI 콘텐츠 이미지 월드컵 애플리케이션
|
3 |
|
4 |
+
��� 애플리케이션은 사용자가 입력한 텍스트 기반으로 이미지를 생성하고,
|
5 |
+
생성된 이미지들로 월드컵 형식의 대결을 진행할 수 있는 인터페이스를 제공합니다.
|
6 |
+
"""
|
7 |
|
8 |
+
import gradio as gr
|
9 |
+
import os
|
10 |
+
from config import APP_TITLE, EXAMPLE_ENTRY_URLS
|
11 |
+
from ui import create_image_tab, create_worldcup_tab, CSS, theme
|
12 |
|
13 |
+
gr.set_static_paths(paths=["static/"])
|
|
|
|
|
|
|
|
|
|
|
14 |
|
15 |
|
16 |
def setup_demo():
|
17 |
+
"""
|
18 |
+
Gradio 인터페이스를 설정하는 함수
|
19 |
+
"""
|
20 |
with gr.Blocks(theme=theme, css=CSS) as demo:
|
21 |
+
# 상태 변수 초기화
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
22 |
gr.Markdown(APP_TITLE)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
23 |
|
24 |
+
# 이미지 생성 탭 생성
|
25 |
+
entryImageUrls = create_image_tab()
|
26 |
|
27 |
+
# 월드컵 탭 생성
|
28 |
+
create_worldcup_tab(entryImageUrls)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
29 |
|
30 |
return demo
|
31 |
|
32 |
|
33 |
+
# 애플리케이션을 시작할 때 사용하는 데모 인스턴스
|
34 |
demo = setup_demo()
|
35 |
|
|
|
|
|
36 |
if __name__ == "__main__":
|
37 |
demo.launch(
|
38 |
auth=("daekyo", os.environ.get("DEFAULT_PW")),
|
39 |
server_name="0.0.0.0",
|
40 |
ssr_mode=False,
|
41 |
)
|
|
|
|
|
|
|
|
|
|
config.py
ADDED
@@ -0,0 +1,101 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
설정 및 상수 정의 파일
|
3 |
+
"""
|
4 |
+
|
5 |
+
from dotenv import load_dotenv
|
6 |
+
import os
|
7 |
+
|
8 |
+
# .env 파일 로드
|
9 |
+
load_dotenv()
|
10 |
+
|
11 |
+
# 월드컵 관련 상수
|
12 |
+
FINAL_ROUND = "## <center>결승전</center>"
|
13 |
+
VS_IMAGE = "<img src='/gradio_api/file=static/image/vs.png' style='margin-top: 100px;'>"
|
14 |
+
WORLDCUPABLE_IMAGE_CNT = [4, 8, 16]
|
15 |
+
BATTLE_IMAGE_INIT = "battle-image fade-in-visibility"
|
16 |
+
|
17 |
+
# 이미지 생성 관련 상수
|
18 |
+
HIGH_PRICE_OPTION = "비싼거(60원/장)"
|
19 |
+
LOW_PRICE_OPTION = "싼거(5원/장)"
|
20 |
+
HIGH_PRICE = 60
|
21 |
+
LOW_PRICE = 5
|
22 |
+
CUSTOM = "custom(비싼모델전용)"
|
23 |
+
|
24 |
+
# 예제 이미지 URL
|
25 |
+
EXAMPLE_SPRING_URLS = [
|
26 |
+
"static/examples/out-1.webp",
|
27 |
+
"static/examples/out-2.webp",
|
28 |
+
"static/examples/out-3.webp",
|
29 |
+
"static/examples/out-4.webp",
|
30 |
+
"static/examples/out-5.webp",
|
31 |
+
"static/examples/out-6.webp",
|
32 |
+
"static/examples/out-7.webp",
|
33 |
+
"static/examples/out-0.webp",
|
34 |
+
]
|
35 |
+
|
36 |
+
EXAMPLE_BEAUTY_URLS = [
|
37 |
+
"static/beauty/out-1.webp",
|
38 |
+
"static/beauty/out-2.webp",
|
39 |
+
"static/beauty/out-3.webp",
|
40 |
+
"static/beauty/out-4.webp",
|
41 |
+
"static/beauty/out-5.webp",
|
42 |
+
"static/beauty/out-6.webp",
|
43 |
+
"static/beauty/out-7.webp",
|
44 |
+
"static/beauty/out-0.webp",
|
45 |
+
]
|
46 |
+
|
47 |
+
EXAMPLE_ENTRY_URLS = []
|
48 |
+
# EXAMPLE_ENTRY_URLS = EXAMPLE_BEAUTY_URLS + EXAMPLE_SPRING_URLS
|
49 |
+
# shuffle(EXAMPLE_ENTRY_URLS)
|
50 |
+
|
51 |
+
# 샘플 프롬프트
|
52 |
+
examplePrompt = "A breathtaking beautiful Korean woman."
|
53 |
+
|
54 |
+
# 이미지 스타일 맵핑
|
55 |
+
imageStyleMap = {
|
56 |
+
"알아서": "Something that you think is the best.",
|
57 |
+
"실사": "photo-realistic",
|
58 |
+
"2D애니": "Classic 90s Disney animation style, 2D hand-drawn aesthetic, rich color palette, traditional cel animation look, expressive character design, detailed painted backgrounds, dramatic lighting, Disney Renaissance era, vibrant scenery, soft shadows, cinematic composition, animated features, warm color tones, characterized by Glen Keane and Mark Henn artistic influence, Disney traditional animation techniques, theatrical quality.",
|
59 |
+
"일본2D애니": "Studio Ghibli style, Hayao Miyazaki aesthetic, hand-drawn animation, whimsical natural environments, soft pastel color palette, detailed background art, nostalgic lighting, ethereal atmosphere, fantastical elements blended with realism, delicate line work",
|
60 |
+
"3D애니메이션": "3d_animation, Disney Pixar",
|
61 |
+
"웹툰": "webtoon, manga",
|
62 |
+
"일러스트": "illustration, Hand-drawn art, Artwork",
|
63 |
+
}
|
64 |
+
|
65 |
+
# 이미지 모델 맵핑
|
66 |
+
imageModelMap = {
|
67 |
+
HIGH_PRICE_OPTION: "black-forest-labs/flux-1.1-pro",
|
68 |
+
LOW_PRICE_OPTION: "black-forest-labs/flux-schnell",
|
69 |
+
}
|
70 |
+
|
71 |
+
# 에플리케이션 UI 텍스트
|
72 |
+
APP_TITLE = """
|
73 |
+
<div style="display: flex; align-items: center; justify-content: space-between; width: 100%">
|
74 |
+
<div style="display: flex; align-items: center; gap: 10px">
|
75 |
+
<picture>
|
76 |
+
<source srcset="/gradio_api/file=static/image/daekyo_logo_kr_white.png" media="(prefers-color-scheme: dark)">
|
77 |
+
<img src="/gradio_api/file=static/image/daekyo_logo_kr.png" width='70' height='70'/>
|
78 |
+
</picture>
|
79 |
+
<span style="font-size: 40px; font-weight: bold; margin-left: 10px;"> 대교 AI 콘텐츠 이미지 월드컵 🏆</span>
|
80 |
+
</div>
|
81 |
+
<a href="/logout" style="text-decoration: none;">
|
82 |
+
<button style="padding: 8px 16px; font-size: 16px; background-color: #446699; color: white; border: none; border-radius: 4px; cursor: pointer;">Logout</button>
|
83 |
+
</a>
|
84 |
+
</div>
|
85 |
+
"""
|
86 |
+
|
87 |
+
# 이미지 생성 샘플 텍스트
|
88 |
+
GEN_IMAGE_DESC = """봄을 담은 정원
|
89 |
+
It was the start of spring.
|
90 |
+
Our teacher had a great idea.
|
91 |
+
"Let's make a vegetable garden behind the school!"
|
92 |
+
We all worked together.
|
93 |
+
First, we dug holes.
|
94 |
+
Next, we planted different seeds.
|
95 |
+
After that, we watered the garden every day.
|
96 |
+
Tiny plants soon appeared in the soil.
|
97 |
+
Slowly, they got bigger and bigger.
|
98 |
+
Finally, we picked the vegetables!
|
99 |
+
There were tomatoes, lettuce, and carrots.
|
100 |
+
We cut them into small pieces and made a colorful salad!
|
101 |
+
It was fresh and delicious. """
|
image_generator.py
ADDED
@@ -0,0 +1,190 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
이미지 생성 관련 기능을 담당하는 모듈
|
3 |
+
"""
|
4 |
+
|
5 |
+
import replicate
|
6 |
+
from config import (
|
7 |
+
imageStyleMap,
|
8 |
+
imageModelMap,
|
9 |
+
HIGH_PRICE_OPTION,
|
10 |
+
LOW_PRICE_OPTION,
|
11 |
+
HIGH_PRICE,
|
12 |
+
LOW_PRICE,
|
13 |
+
)
|
14 |
+
|
15 |
+
|
16 |
+
def to_string(output):
|
17 |
+
"""
|
18 |
+
output 을 하나의 문자열로 합치기.
|
19 |
+
"""
|
20 |
+
combined_result = ""
|
21 |
+
for item in output:
|
22 |
+
combined_result += str(item)
|
23 |
+
return combined_result
|
24 |
+
|
25 |
+
|
26 |
+
def genFluxPrompt(userPrompt, additionalComment, ratio, imageStyle):
|
27 |
+
"""
|
28 |
+
userPrompt 와 additionalComment 를 받아서 비율에 맞는 이미지 생성을 위한 prompt 생성.
|
29 |
+
"""
|
30 |
+
imageStyle = imageStyleMap[imageStyle]
|
31 |
+
# print("genFluxPrompt ", userPrompt, ratio, imageStyle)
|
32 |
+
# The ibm-granite/granite-3.1-8b-instruct model can stream output as it's running.
|
33 |
+
|
34 |
+
additionalComment = (
|
35 |
+
f"\n# Important note: {additionalComment}\n" if additionalComment else ""
|
36 |
+
)
|
37 |
+
|
38 |
+
prompt = (
|
39 |
+
f"Please create a prompt that will generate a best quality image that can express the following sentence in English.(DO NOT INCLUDE ANY KOREAN LANGUAGE.)"
|
40 |
+
f"\n# Image generation info\n"
|
41 |
+
f"\n- Aspect ratio: {ratio}\n"
|
42 |
+
f"\n- Image style: {imageStyle}\n"
|
43 |
+
"\n# Description for the image.\n" + userPrompt + additionalComment
|
44 |
+
)
|
45 |
+
|
46 |
+
print("genFluxPrompt prompt:", prompt)
|
47 |
+
|
48 |
+
result = replicate.run(
|
49 |
+
"anthropic/claude-3.5-haiku",
|
50 |
+
input={
|
51 |
+
"top_k": 50,
|
52 |
+
"top_p": 0.9,
|
53 |
+
"prompt": prompt,
|
54 |
+
"max_tokens": 256,
|
55 |
+
"min_tokens": 0,
|
56 |
+
"temperature": 0.6,
|
57 |
+
"system_prompt": (
|
58 |
+
"You are a professional prompt engineer specializing in creating prompts for txt2img AI model."
|
59 |
+
"You always create prompts that can extract the impressive output."
|
60 |
+
"Make sure to emphasize the intention for 'Important note:' section in the prompts if it exists."
|
61 |
+
"Do not generate negative prompts!"
|
62 |
+
),
|
63 |
+
"presence_penalty": 0,
|
64 |
+
"frequency_penalty": 0,
|
65 |
+
},
|
66 |
+
)
|
67 |
+
prompt = to_string(result)
|
68 |
+
print("genFluxPrompt prompt:", prompt)
|
69 |
+
return prompt
|
70 |
+
|
71 |
+
|
72 |
+
def genImage(prompt, ratio, imageModelKind, imgWidth, imgHeight, imageNum=1):
|
73 |
+
"""
|
74 |
+
prompt 를 받아서 비율에 맞는 이미지 imageNum 만큼 생성.
|
75 |
+
"""
|
76 |
+
|
77 |
+
imageModel = imageModelMap[imageModelKind]
|
78 |
+
|
79 |
+
if ratio == "custom":
|
80 |
+
input = {
|
81 |
+
"prompt": prompt,
|
82 |
+
"aspect_ratio": ratio, # gradio radio 버튼에서 선택된 값
|
83 |
+
"width": int(imgWidth),
|
84 |
+
"height": int(imgHeight),
|
85 |
+
"aspect_ratio": "custom",
|
86 |
+
"output_format": "webp",
|
87 |
+
"output_quality": 90,
|
88 |
+
}
|
89 |
+
# custom 일 때는 비싼 모델로 생성.
|
90 |
+
imageModel = imageModelMap[HIGH_PRICE_OPTION]
|
91 |
+
else:
|
92 |
+
input = {
|
93 |
+
"prompt": prompt,
|
94 |
+
"aspect_ratio": ratio, # gradio radio 버튼에서 선택된 값
|
95 |
+
"output_format": "webp",
|
96 |
+
"output_quality": 90,
|
97 |
+
"num_outputs": imageNum,
|
98 |
+
}
|
99 |
+
|
100 |
+
print("genImage input:", input)
|
101 |
+
|
102 |
+
imageModel = imageModelMap[imageModelKind]
|
103 |
+
result = replicate.run(imageModel, input=input)
|
104 |
+
|
105 |
+
print("Generated Image Info:", result)
|
106 |
+
# imgUrls = [str(img) for img in result]
|
107 |
+
# imgUrl = str(result) if type(result) != list else str(result[0])
|
108 |
+
imgUrl = str(result) if not isinstance(result, list) else str(result[0])
|
109 |
+
if imageNum > 1:
|
110 |
+
imgUrls = [str(img) for img in result]
|
111 |
+
print("genImage imgUrls:", imgUrls)
|
112 |
+
return imgUrls
|
113 |
+
else:
|
114 |
+
print("genImage imgUrl:", imgUrl)
|
115 |
+
return imgUrl
|
116 |
+
|
117 |
+
|
118 |
+
def reduxImage(prompt, url):
|
119 |
+
"""
|
120 |
+
url 의 이미지를 재생성.
|
121 |
+
|
122 |
+
output = replicate.run(
|
123 |
+
"black-forest-labs/flux-1.1-pro",
|
124 |
+
input={
|
125 |
+
"width": 1024,
|
126 |
+
"height": 480,
|
127 |
+
"prompt": "one boy and 3 girls hands and hands playing in the school.",
|
128 |
+
"aspect_ratio": "custom",
|
129 |
+
"image_prompt": "https://replicate.delivery/xezq/CLKVkX1QXeXrMy1tH43zJmCk6RgBszqmQdK45AOUTedCkyUUA/tmpucuderjn.webp",
|
130 |
+
"output_format": "webp",
|
131 |
+
"output_quality": 80,
|
132 |
+
"safety_tolerance": 2,
|
133 |
+
"prompt_upsampling": True
|
134 |
+
}
|
135 |
+
)
|
136 |
+
"""
|
137 |
+
result = replicate.run(
|
138 |
+
"black-forest-labs/flux-1.1-pro",
|
139 |
+
input={
|
140 |
+
"prompt": prompt,
|
141 |
+
"aspect_ratio": "16:9",
|
142 |
+
"image_prompt": url,
|
143 |
+
"output_format": "webp",
|
144 |
+
"output_quality": 80,
|
145 |
+
"safety_tolerance": 2, # 1 is most strict and 6 is most permissive
|
146 |
+
"prompt_upsampling": True,
|
147 |
+
},
|
148 |
+
)
|
149 |
+
return str(result)
|
150 |
+
|
151 |
+
|
152 |
+
def genPromptAndImage(
|
153 |
+
userPrompt,
|
154 |
+
additionalComment,
|
155 |
+
fluxPrompt,
|
156 |
+
ratio,
|
157 |
+
imageStyle,
|
158 |
+
imageModel,
|
159 |
+
highImageCnt,
|
160 |
+
lowImageCnt,
|
161 |
+
imgWidth,
|
162 |
+
imgHeight,
|
163 |
+
):
|
164 |
+
"""
|
165 |
+
fluxPrompt 가 빈 문자열이면 새로운 prompt 를 생성하고,
|
166 |
+
빈 문자열이 아니면 기존의 prompt 를 사용해서 이미지 생성.
|
167 |
+
"""
|
168 |
+
print(
|
169 |
+
"genPromptAndImage:",
|
170 |
+
ratio,
|
171 |
+
imageStyle,
|
172 |
+
imageModel,
|
173 |
+
highImageCnt,
|
174 |
+
lowImageCnt,
|
175 |
+
)
|
176 |
+
|
177 |
+
if fluxPrompt == "":
|
178 |
+
fluxPrompt = genFluxPrompt(userPrompt, additionalComment, ratio, imageStyle)
|
179 |
+
|
180 |
+
imgUrl = genImage(fluxPrompt, ratio, imageModel, imgWidth, imgHeight)
|
181 |
+
|
182 |
+
highImageCnt = int(highImageCnt)
|
183 |
+
lowImageCnt = int(lowImageCnt)
|
184 |
+
if imageModel == HIGH_PRICE_OPTION:
|
185 |
+
highImageCnt += 1
|
186 |
+
else:
|
187 |
+
lowImageCnt += 1
|
188 |
+
|
189 |
+
totalPrice = highImageCnt * HIGH_PRICE + lowImageCnt * LOW_PRICE
|
190 |
+
return fluxPrompt, highImageCnt, lowImageCnt, totalPrice, imgUrl, None
|
requirements.txt
CHANGED
@@ -1,27 +1,27 @@
|
|
1 |
aiofiles==23.2.1
|
2 |
annotated-types==0.7.0
|
3 |
-
anyio==4.
|
4 |
certifi==2025.1.31
|
5 |
charset-normalizer==3.4.1
|
6 |
click==8.1.8
|
7 |
dotenv==0.9.9
|
8 |
fastapi==0.115.11
|
9 |
ffmpy==0.5.0
|
10 |
-
filelock==3.
|
11 |
-
fsspec==2025.
|
12 |
-
gradio==5.
|
13 |
-
gradio_client==1.
|
14 |
groovy==0.1.2
|
15 |
h11==0.14.0
|
16 |
httpcore==1.0.7
|
17 |
httpx==0.28.1
|
18 |
-
huggingface-hub==0.29.
|
19 |
idna==3.10
|
20 |
-
Jinja2==3.1.
|
21 |
markdown-it-py==3.0.0
|
22 |
MarkupSafe==2.1.5
|
23 |
mdurl==0.1.2
|
24 |
-
numpy==2.2.
|
25 |
orjson==3.10.15
|
26 |
packaging==24.2
|
27 |
pandas==2.2.3
|
@@ -32,18 +32,19 @@ pydub==0.25.1
|
|
32 |
Pygments==2.19.1
|
33 |
python-dateutil==2.9.0.post0
|
34 |
python-dotenv==1.0.1
|
|
|
35 |
pytz==2025.1
|
36 |
PyYAML==6.0.2
|
37 |
replicate==1.0.4
|
38 |
requests==2.32.3
|
39 |
rich==13.9.4
|
40 |
-
ruff==0.
|
41 |
safehttpx==0.1.6
|
42 |
semantic-version==2.10.0
|
43 |
shellingham==1.5.4
|
44 |
six==1.17.0
|
45 |
sniffio==1.3.1
|
46 |
-
starlette==0.46.
|
47 |
tomlkit==0.13.2
|
48 |
tqdm==4.67.1
|
49 |
typer==0.15.2
|
@@ -51,4 +52,4 @@ typing_extensions==4.12.2
|
|
51 |
tzdata==2025.1
|
52 |
urllib3==2.3.0
|
53 |
uvicorn==0.34.0
|
54 |
-
websockets==15.0
|
|
|
1 |
aiofiles==23.2.1
|
2 |
annotated-types==0.7.0
|
3 |
+
anyio==4.9.0
|
4 |
certifi==2025.1.31
|
5 |
charset-normalizer==3.4.1
|
6 |
click==8.1.8
|
7 |
dotenv==0.9.9
|
8 |
fastapi==0.115.11
|
9 |
ffmpy==0.5.0
|
10 |
+
filelock==3.18.0
|
11 |
+
fsspec==2025.3.0
|
12 |
+
gradio==5.22.0
|
13 |
+
gradio_client==1.8.0
|
14 |
groovy==0.1.2
|
15 |
h11==0.14.0
|
16 |
httpcore==1.0.7
|
17 |
httpx==0.28.1
|
18 |
+
huggingface-hub==0.29.3
|
19 |
idna==3.10
|
20 |
+
Jinja2==3.1.6
|
21 |
markdown-it-py==3.0.0
|
22 |
MarkupSafe==2.1.5
|
23 |
mdurl==0.1.2
|
24 |
+
numpy==2.2.4
|
25 |
orjson==3.10.15
|
26 |
packaging==24.2
|
27 |
pandas==2.2.3
|
|
|
32 |
Pygments==2.19.1
|
33 |
python-dateutil==2.9.0.post0
|
34 |
python-dotenv==1.0.1
|
35 |
+
python-multipart==0.0.20
|
36 |
pytz==2025.1
|
37 |
PyYAML==6.0.2
|
38 |
replicate==1.0.4
|
39 |
requests==2.32.3
|
40 |
rich==13.9.4
|
41 |
+
ruff==0.11.2
|
42 |
safehttpx==0.1.6
|
43 |
semantic-version==2.10.0
|
44 |
shellingham==1.5.4
|
45 |
six==1.17.0
|
46 |
sniffio==1.3.1
|
47 |
+
starlette==0.46.1
|
48 |
tomlkit==0.13.2
|
49 |
tqdm==4.67.1
|
50 |
typer==0.15.2
|
|
|
52 |
tzdata==2025.1
|
53 |
urllib3==2.3.0
|
54 |
uvicorn==0.34.0
|
55 |
+
websockets==15.0.1
|
ui/__init__.py
ADDED
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
UI 패키지 초기화 파일
|
3 |
+
"""
|
4 |
+
|
5 |
+
from ui.image_gen_tab import create_image_tab
|
6 |
+
from ui.worldcup_tab import create_worldcup_tab
|
7 |
+
from ui.styles import CSS, theme
|
ui/css/base.css
ADDED
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
/* 기본 CSS 설정 */
|
2 |
+
footer {
|
3 |
+
visibility: hidden;
|
4 |
+
}
|
5 |
+
|
6 |
+
.tiny-input {
|
7 |
+
width: "20px";
|
8 |
+
}
|
ui/css/fade-in.css
ADDED
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
/***********************************************/
|
2 |
+
/* 이미지 강조를 위해 키우는 애니메이션 */
|
3 |
+
/***********************************************/
|
4 |
+
/* CSS for fade-in animation with scale */
|
5 |
+
.fade-in {
|
6 |
+
animation: fadeInAndScale 0.3s ease forwards;
|
7 |
+
/* Add these properties to ensure smooth animation */
|
8 |
+
transform-origin: center;
|
9 |
+
display: inline-block;
|
10 |
+
opacity: 0; /* Start with opacity 0 */
|
11 |
+
}
|
12 |
+
|
13 |
+
@keyframes fadeInAndScale {
|
14 |
+
0% {
|
15 |
+
opacity: 0;
|
16 |
+
transform: scale(1);
|
17 |
+
}
|
18 |
+
100% {
|
19 |
+
opacity: 1;
|
20 |
+
transform: scale(1.15); /* Element grows to 1.15 times its original size */
|
21 |
+
}
|
22 |
+
}
|
ui/css/fade-out.css
ADDED
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
/***********************************************/
|
2 |
+
/* 이미지 사라지는 애니메이션 */
|
3 |
+
/***********************************************/
|
4 |
+
/* CSS for fade-out animation */
|
5 |
+
.fade-out {
|
6 |
+
animation: fadeOutAndShrink 1s forwards;
|
7 |
+
/* Add these properties to ensure smooth animation */
|
8 |
+
transform-origin: center;
|
9 |
+
display: inline-block;
|
10 |
+
}
|
11 |
+
|
12 |
+
@keyframes fadeOutAndShrink {
|
13 |
+
0% {
|
14 |
+
opacity: 1;
|
15 |
+
transform: scale(1);
|
16 |
+
}
|
17 |
+
100% {
|
18 |
+
opacity: 0;
|
19 |
+
transform: scale(0);
|
20 |
+
}
|
21 |
+
}
|
22 |
+
|
23 |
+
/* Optional: Add this if you want to keep the layout space after element disappears */
|
24 |
+
.fade-out.preserve-space {
|
25 |
+
visibility: hidden;
|
26 |
+
opacity: 0;
|
27 |
+
animation: fadeOutAndShrinkPreserve 1s forwards;
|
28 |
+
}
|
29 |
+
|
30 |
+
@keyframes fadeOutAndShrinkPreserve {
|
31 |
+
0% {
|
32 |
+
opacity: 1;
|
33 |
+
transform: scale(1);
|
34 |
+
visibility: visible;
|
35 |
+
}
|
36 |
+
99% {
|
37 |
+
transform: scale(0.01);
|
38 |
+
opacity: 0;
|
39 |
+
visibility: visible;
|
40 |
+
}
|
41 |
+
100% {
|
42 |
+
transform: scale(0);
|
43 |
+
opacity: 0;
|
44 |
+
visibility: hidden;
|
45 |
+
}
|
46 |
+
}
|
ui/css/fade-visibility.css
ADDED
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
/***********************************************/
|
2 |
+
/* 처음에는 안보이다가 점점 나타나는 애니메이션 */
|
3 |
+
/***********************************************/
|
4 |
+
.fade-in-visibility {
|
5 |
+
/* 처음에는 보이지 않음 */
|
6 |
+
opacity: 0;
|
7 |
+
/* 애니메이션 설정 */
|
8 |
+
animation: fadeInVisibility 1s ease-in forwards;
|
9 |
+
/* 애니메이션을 부드럽게 만들기 위한 설정 */
|
10 |
+
will-change: opacity;
|
11 |
+
}
|
12 |
+
|
13 |
+
@keyframes fadeInVisibility {
|
14 |
+
0% {
|
15 |
+
opacity: 0;
|
16 |
+
}
|
17 |
+
100% {
|
18 |
+
opacity: 1;
|
19 |
+
}
|
20 |
+
}
|
ui/css/interactions.css
ADDED
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
/* 이미지 클릭할 때, 포인터 커서로 변경 */
|
2 |
+
.battle-image img {
|
3 |
+
cursor: pointer !important;
|
4 |
+
}
|
5 |
+
|
6 |
+
/***********************************************/
|
7 |
+
/* 이미지 클릭 비활성화 */
|
8 |
+
/***********************************************/
|
9 |
+
.non-clickable {
|
10 |
+
pointer-events: none; /* 모든 포인터 이벤트(클릭, 호버 등)를 비활성화 */
|
11 |
+
user-select: none; /* 텍스트 선택 방지 */
|
12 |
+
}
|
ui/image_gen_tab.py
ADDED
@@ -0,0 +1,248 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
이미지 생성 UI 컴포넌트 및 기능
|
3 |
+
"""
|
4 |
+
|
5 |
+
import gradio as gr
|
6 |
+
import time
|
7 |
+
from config import (
|
8 |
+
HIGH_PRICE_OPTION,
|
9 |
+
LOW_PRICE_OPTION,
|
10 |
+
CUSTOM,
|
11 |
+
GEN_IMAGE_DESC,
|
12 |
+
WORLDCUPABLE_IMAGE_CNT,
|
13 |
+
imageStyleMap,
|
14 |
+
)
|
15 |
+
from image_generator import genPromptAndImage
|
16 |
+
|
17 |
+
|
18 |
+
def create_image_tab():
|
19 |
+
"""이미지 생성 탭 UI 생성 함수"""
|
20 |
+
with gr.Tab("이미지 생성"):
|
21 |
+
current_img_url = gr.State("")
|
22 |
+
selectedImageIndex = gr.State(0)
|
23 |
+
entryImageUrls = gr.State([]) # 저장된 URL 목록
|
24 |
+
|
25 |
+
with gr.Column(): # Column을 사용하여 수직 배열
|
26 |
+
with gr.Row():
|
27 |
+
with gr.Column(): # Column을 사용하여 수직 배열
|
28 |
+
userPrompt = gr.TextArea(
|
29 |
+
label="교재 지문 (필수)",
|
30 |
+
value=GEN_IMAGE_DESC,
|
31 |
+
max_lines=5,
|
32 |
+
)
|
33 |
+
additionalComment = gr.Textbox(
|
34 |
+
label="추가 희망사항 (옵셔널)", lines=2, value=""
|
35 |
+
)
|
36 |
+
with gr.Column():
|
37 |
+
with gr.Accordion("상세 설정", open=False):
|
38 |
+
with gr.Column(): # Column을 사용하여 수직 배열
|
39 |
+
with gr.Row():
|
40 |
+
highImgCnt = gr.Textbox(
|
41 |
+
label="비싼 이미지",
|
42 |
+
value="0",
|
43 |
+
interactive=False,
|
44 |
+
)
|
45 |
+
lowImgCnt = gr.Textbox(
|
46 |
+
label="싼 이미지", value="0", interactive=False
|
47 |
+
)
|
48 |
+
totalPrice = gr.Textbox(
|
49 |
+
label="총 이미지 생성 비용(원)",
|
50 |
+
value="0",
|
51 |
+
interactive=False,
|
52 |
+
)
|
53 |
+
genModelKind = gr.Radio(
|
54 |
+
[LOW_PRICE_OPTION, HIGH_PRICE_OPTION],
|
55 |
+
label="이미지 생성 모델",
|
56 |
+
value=LOW_PRICE_OPTION,
|
57 |
+
)
|
58 |
+
with gr.Row():
|
59 |
+
aspectRatio = gr.Radio(
|
60 |
+
["1:1", "4:3", "16:9", CUSTOM],
|
61 |
+
label="이미지 비율",
|
62 |
+
value="16:9",
|
63 |
+
scale=4,
|
64 |
+
)
|
65 |
+
imgWidth = gr.Number(
|
66 |
+
label="너비 (32의배수)",
|
67 |
+
value=1440,
|
68 |
+
scale=1,
|
69 |
+
visible=False,
|
70 |
+
maximum=1440,
|
71 |
+
minimum=256,
|
72 |
+
step=32,
|
73 |
+
)
|
74 |
+
imgHeight = gr.Number(
|
75 |
+
label="높이 (32의배수)",
|
76 |
+
value=768,
|
77 |
+
scale=1,
|
78 |
+
visible=False,
|
79 |
+
maximum=1440,
|
80 |
+
minimum=256,
|
81 |
+
step=32,
|
82 |
+
)
|
83 |
+
|
84 |
+
def custom_aspect_ratio(ratio):
|
85 |
+
if ratio == CUSTOM:
|
86 |
+
return (
|
87 |
+
gr.update(
|
88 |
+
value=HIGH_PRICE_OPTION,
|
89 |
+
interactive=False,
|
90 |
+
),
|
91 |
+
gr.update(visible=True),
|
92 |
+
gr.update(visible=True),
|
93 |
+
)
|
94 |
+
else:
|
95 |
+
return (
|
96 |
+
gr.update(interactive=True),
|
97 |
+
gr.update(visible=False),
|
98 |
+
gr.update(visible=False),
|
99 |
+
)
|
100 |
+
|
101 |
+
aspectRatio.change(
|
102 |
+
fn=custom_aspect_ratio,
|
103 |
+
inputs=aspectRatio,
|
104 |
+
outputs=[genModelKind, imgWidth, imgHeight],
|
105 |
+
)
|
106 |
+
|
107 |
+
styleList = list(imageStyleMap.keys())
|
108 |
+
imageStyle = gr.Radio(
|
109 |
+
styleList, label="이미지 스타일", value="알아서"
|
110 |
+
)
|
111 |
+
with gr.Accordion("Prompt info", open=False):
|
112 |
+
fluxPrompt = gr.Textbox(
|
113 |
+
label="Flux Prompt", value="", lines=10
|
114 |
+
)
|
115 |
+
|
116 |
+
with gr.Row():
|
117 |
+
genSmartImage = gr.Button("교재 지문 삽화 이미지 생성!")
|
118 |
+
removeEntryWorldcupBtn = gr.Button("현재 이미지 제거")
|
119 |
+
|
120 |
+
with gr.Column():
|
121 |
+
progressBar = gr.Image(
|
122 |
+
label=None, interactive=False, visible=False, height=28
|
123 |
+
)
|
124 |
+
entryListGallery = gr.Gallery(
|
125 |
+
label="이미지 월드컵 진출 이미지",
|
126 |
+
preview=True,
|
127 |
+
allow_preview=True,
|
128 |
+
interactive=False,
|
129 |
+
)
|
130 |
+
with gr.Row():
|
131 |
+
entryCount = gr.Markdown("## 이미지 월드컵 진출 이미지 수: 0")
|
132 |
+
goImageWorldCupBtn = gr.Button(
|
133 |
+
"이미지 월드컵 하러가기", visible=False
|
134 |
+
)
|
135 |
+
|
136 |
+
@entryListGallery.select(outputs=selectedImageIndex)
|
137 |
+
def setSelectedImageIndex(evt: gr.SelectData, state=None):
|
138 |
+
try:
|
139 |
+
if evt is None:
|
140 |
+
return 0
|
141 |
+
return evt.index
|
142 |
+
except:
|
143 |
+
return 0
|
144 |
+
|
145 |
+
def removeSelectedEntry(entryList, selectedIndex):
|
146 |
+
if selectedIndex is None or len(entryList) == 0:
|
147 |
+
return gr.update(value=entryList), entryList
|
148 |
+
|
149 |
+
del entryList[selectedIndex]
|
150 |
+
return (
|
151 |
+
entryList,
|
152 |
+
entryList,
|
153 |
+
f"## 이미지 월드컵 진출 이미지 수: {len(entryList)}",
|
154 |
+
)
|
155 |
+
|
156 |
+
# 왠지 모르게 마지막 요소를 지우면 preview=True 가 안되서 이렇게 함.
|
157 |
+
def reSelectedEntry(entryList):
|
158 |
+
return gr.update(selected_index=len(entryList) - 1, preview=True)
|
159 |
+
|
160 |
+
removeEntryWorldcupBtn.click(
|
161 |
+
fn=removeSelectedEntry,
|
162 |
+
inputs=[entryImageUrls, selectedImageIndex],
|
163 |
+
outputs=[entryListGallery, entryImageUrls, entryCount],
|
164 |
+
).then(
|
165 |
+
fn=reSelectedEntry,
|
166 |
+
inputs=entryImageUrls,
|
167 |
+
outputs=entryListGallery,
|
168 |
+
)
|
169 |
+
|
170 |
+
# 이벤트 처리.
|
171 |
+
# 입력값이 변경될 때마다 Flux Prompt 초기화
|
172 |
+
for component in [
|
173 |
+
userPrompt,
|
174 |
+
aspectRatio,
|
175 |
+
imageStyle,
|
176 |
+
additionalComment,
|
177 |
+
]:
|
178 |
+
component.change(fn=lambda x: "", inputs=fluxPrompt, outputs=fluxPrompt)
|
179 |
+
genImageInputs = [
|
180 |
+
userPrompt,
|
181 |
+
additionalComment,
|
182 |
+
fluxPrompt,
|
183 |
+
aspectRatio,
|
184 |
+
imageStyle,
|
185 |
+
genModelKind,
|
186 |
+
highImgCnt,
|
187 |
+
lowImgCnt,
|
188 |
+
imgWidth,
|
189 |
+
imgHeight,
|
190 |
+
]
|
191 |
+
|
192 |
+
def show_progress():
|
193 |
+
return gr.update(label="이미지 생성 중", visible=True)
|
194 |
+
|
195 |
+
def hide_progress():
|
196 |
+
return gr.update(visible=False)
|
197 |
+
|
198 |
+
genSmartImage.click(fn=show_progress, outputs=progressBar).then(
|
199 |
+
fn=genPromptAndImage,
|
200 |
+
inputs=genImageInputs,
|
201 |
+
outputs=[
|
202 |
+
fluxPrompt,
|
203 |
+
highImgCnt,
|
204 |
+
lowImgCnt,
|
205 |
+
totalPrice,
|
206 |
+
current_img_url,
|
207 |
+
progressBar,
|
208 |
+
],
|
209 |
+
).then(
|
210 |
+
fn=lambda entryList, imgUrl: (
|
211 |
+
gr.update(
|
212 |
+
value=[*entryList, imgUrl],
|
213 |
+
preview=True,
|
214 |
+
selected_index=len(entryList),
|
215 |
+
),
|
216 |
+
entryList + [imgUrl],
|
217 |
+
len(entryList),
|
218 |
+
gr.update(visible=False),
|
219 |
+
),
|
220 |
+
inputs=[entryImageUrls, current_img_url],
|
221 |
+
outputs=[
|
222 |
+
entryListGallery,
|
223 |
+
entryImageUrls,
|
224 |
+
selectedImageIndex,
|
225 |
+
progressBar,
|
226 |
+
],
|
227 |
+
).then(
|
228 |
+
fn=lambda entryImageUrls: f"## 이미지 월드컵 진출 이미지 수: {len(entryImageUrls)}",
|
229 |
+
inputs=entryImageUrls,
|
230 |
+
outputs=entryCount,
|
231 |
+
).then(
|
232 |
+
fn=lambda entryCountTxt, entryList: (
|
233 |
+
[
|
234 |
+
entryCountTxt + " (이미지 월드컵 가능!)",
|
235 |
+
gr.update(visible=False),
|
236 |
+
gr.Info(
|
237 |
+
f"{len(entryList)}개의 이미지가 만들어졌습니다. '이미지 월드컵'을 시작하실 수 있습니다!",
|
238 |
+
duration=5,
|
239 |
+
),
|
240 |
+
]
|
241 |
+
if len(entryList) in WORLDCUPABLE_IMAGE_CNT
|
242 |
+
else [entryCountTxt, gr.update(visible=False)]
|
243 |
+
),
|
244 |
+
inputs=[entryCount, entryImageUrls],
|
245 |
+
outputs=[entryCount, goImageWorldCupBtn],
|
246 |
+
)
|
247 |
+
|
248 |
+
return entryImageUrls
|
ui/styles.py
ADDED
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
UI 스타일 관련 정의
|
3 |
+
"""
|
4 |
+
|
5 |
+
import gradio as gr
|
6 |
+
import os
|
7 |
+
|
8 |
+
# UI 기본 테마 설정
|
9 |
+
theme = "base" # 기본 테마 사용
|
10 |
+
|
11 |
+
# CSS 디렉토리 경로 설정
|
12 |
+
css_dir = os.path.join(os.path.dirname(__file__), "css")
|
13 |
+
|
14 |
+
|
15 |
+
def read_css_file(css_file):
|
16 |
+
"""CSS 파일명을 받아 파일 내용을 반환하는 함수
|
17 |
+
|
18 |
+
Args:
|
19 |
+
css_file (str): CSS 파일 이름 (예: "base.css")
|
20 |
+
|
21 |
+
Returns:
|
22 |
+
str: CSS 파일 내용
|
23 |
+
"""
|
24 |
+
try:
|
25 |
+
file_path = os.path.join(css_dir, css_file)
|
26 |
+
with open(file_path, "r", encoding="utf-8") as file:
|
27 |
+
return file.read()
|
28 |
+
except Exception as e:
|
29 |
+
print(f"CSS 파일 읽기 오류 ({css_file}): {e}")
|
30 |
+
return ""
|
31 |
+
|
32 |
+
|
33 |
+
# CSS 파일 내용 가져오기
|
34 |
+
base_css = read_css_file("base.css")
|
35 |
+
interactions_css = read_css_file("interactions.css")
|
36 |
+
|
37 |
+
# 애니메이션 CSS 파일 내용 가져오기
|
38 |
+
fade_out_css = read_css_file("fade-out.css")
|
39 |
+
fade_in_css = read_css_file("fade-in.css")
|
40 |
+
fade_visibility_css = read_css_file("fade-visibility.css")
|
41 |
+
|
42 |
+
# 모든 CSS 통합
|
43 |
+
CSS = f"""
|
44 |
+
{base_css}
|
45 |
+
|
46 |
+
{interactions_css}
|
47 |
+
|
48 |
+
{fade_out_css}
|
49 |
+
|
50 |
+
{fade_in_css}
|
51 |
+
|
52 |
+
{fade_visibility_css}
|
53 |
+
"""
|
ui/worldcup_tab.py
ADDED
@@ -0,0 +1,185 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
월드컵 UI 컴포넌트 및 기능
|
3 |
+
"""
|
4 |
+
|
5 |
+
import gradio as gr
|
6 |
+
import time
|
7 |
+
from worldcup import Worldcup
|
8 |
+
from config import VS_IMAGE, BATTLE_IMAGE_INIT, WORLDCUPABLE_IMAGE_CNT
|
9 |
+
|
10 |
+
|
11 |
+
def create_worldcup_tab(entryImageUrls):
|
12 |
+
"""월드컵 탭 UI 생성 함수"""
|
13 |
+
with gr.Tab("이미지 월드컵"):
|
14 |
+
worldcup = gr.State()
|
15 |
+
with gr.Column(): # Column을 사용하여 수직 배열
|
16 |
+
startWorldcupBtn = gr.Button("월드컵 시작!")
|
17 |
+
|
18 |
+
with gr.Row():
|
19 |
+
winImage1Btn = gr.Button("이미지 1 승리!", scale=5, visible=False)
|
20 |
+
worldcupTitle = gr.Markdown("# ")
|
21 |
+
winImage2Btn = gr.Button("이미지 2 승리!", scale=5, visible=False)
|
22 |
+
|
23 |
+
with gr.Row(): # 이미지 수평 배열.
|
24 |
+
battleImage1 = gr.Image(
|
25 |
+
show_label=False,
|
26 |
+
scale=5,
|
27 |
+
elem_classes="battle-image",
|
28 |
+
interactive=False,
|
29 |
+
show_download_button=False,
|
30 |
+
)
|
31 |
+
vsImage = gr.HTML(VS_IMAGE)
|
32 |
+
battleImage2 = gr.Image(
|
33 |
+
show_label=False,
|
34 |
+
scale=5,
|
35 |
+
elem_classes="battle-image",
|
36 |
+
interactive=False,
|
37 |
+
show_download_button=False,
|
38 |
+
)
|
39 |
+
|
40 |
+
def winImage0(winnerIndex):
|
41 |
+
print("winImage0 winnerIndex:", winnerIndex)
|
42 |
+
if winnerIndex == 0:
|
43 |
+
return gr.update(elem_classes="fade-in non-clickable"), gr.update(
|
44 |
+
elem_classes="fade-out"
|
45 |
+
)
|
46 |
+
else:
|
47 |
+
return gr.update(elem_classes="fade-out"), gr.update(
|
48 |
+
elem_classes="fade-in non-clickable"
|
49 |
+
)
|
50 |
+
|
51 |
+
def sleep():
|
52 |
+
time.sleep(1)
|
53 |
+
|
54 |
+
def winImage(winnerIndex, worldcup):
|
55 |
+
print("winnerIndex:", winnerIndex)
|
56 |
+
print("worldcup:", worldcup)
|
57 |
+
finalWinner = worldcup.winImage(winnerIndex)
|
58 |
+
battleTitle = worldcup.getKangRound()
|
59 |
+
if finalWinner:
|
60 |
+
return (
|
61 |
+
gr.update(
|
62 |
+
visible=winnerIndex == 0,
|
63 |
+
elem_classes=BATTLE_IMAGE_INIT,
|
64 |
+
),
|
65 |
+
gr.update(
|
66 |
+
visible=winnerIndex == 1,
|
67 |
+
elem_classes=BATTLE_IMAGE_INIT,
|
68 |
+
),
|
69 |
+
worldcup,
|
70 |
+
"## <center>🎉 우승 🎊</center>",
|
71 |
+
gr.update(visible=False),
|
72 |
+
gr.update(visible=True),
|
73 |
+
gr.update(visible=False),
|
74 |
+
gr.update(visible=False),
|
75 |
+
)
|
76 |
+
nextMatchImages = worldcup.getCurrentRoundImages()
|
77 |
+
print("nextMatchImages:", nextMatchImages)
|
78 |
+
return (
|
79 |
+
gr.update(
|
80 |
+
value=nextMatchImages[0],
|
81 |
+
elem_classes=BATTLE_IMAGE_INIT,
|
82 |
+
),
|
83 |
+
gr.update(
|
84 |
+
value=nextMatchImages[1],
|
85 |
+
elem_classes=BATTLE_IMAGE_INIT,
|
86 |
+
),
|
87 |
+
worldcup,
|
88 |
+
battleTitle,
|
89 |
+
gr.skip(),
|
90 |
+
gr.skip(),
|
91 |
+
gr.skip(),
|
92 |
+
gr.skip(),
|
93 |
+
)
|
94 |
+
|
95 |
+
wOutputs = [
|
96 |
+
battleImage1,
|
97 |
+
battleImage2,
|
98 |
+
worldcup,
|
99 |
+
worldcupTitle,
|
100 |
+
vsImage,
|
101 |
+
startWorldcupBtn,
|
102 |
+
winImage1Btn,
|
103 |
+
winImage2Btn,
|
104 |
+
]
|
105 |
+
|
106 |
+
for event, winIndex in [
|
107 |
+
(winImage1Btn.click, 0),
|
108 |
+
(winImage2Btn.click, 1),
|
109 |
+
]:
|
110 |
+
event(
|
111 |
+
fn=lambda w, i=winIndex: winImage0(i),
|
112 |
+
inputs=[battleImage1],
|
113 |
+
outputs=[battleImage1, battleImage2],
|
114 |
+
).then(fn=sleep).then(
|
115 |
+
fn=lambda w, i=winIndex: winImage(i, w),
|
116 |
+
inputs=[worldcup],
|
117 |
+
outputs=wOutputs,
|
118 |
+
)
|
119 |
+
|
120 |
+
def handle_image_select(evt, image_index):
|
121 |
+
try:
|
122 |
+
return winImage0(image_index)
|
123 |
+
except:
|
124 |
+
return gr.skip(), gr.skip()
|
125 |
+
|
126 |
+
battleImage1.select(
|
127 |
+
fn=lambda evt: handle_image_select(evt, 0),
|
128 |
+
outputs=[battleImage1, battleImage2],
|
129 |
+
).then(fn=sleep).then(
|
130 |
+
fn=lambda w: winImage(0, w),
|
131 |
+
inputs=[worldcup],
|
132 |
+
outputs=wOutputs,
|
133 |
+
)
|
134 |
+
|
135 |
+
battleImage2.select(
|
136 |
+
fn=lambda evt: handle_image_select(evt, 1),
|
137 |
+
outputs=[battleImage1, battleImage2],
|
138 |
+
).then(fn=sleep).then(
|
139 |
+
fn=lambda w: winImage(1, w),
|
140 |
+
inputs=[worldcup],
|
141 |
+
outputs=wOutputs,
|
142 |
+
)
|
143 |
+
|
144 |
+
@startWorldcupBtn.click(
|
145 |
+
inputs=[entryImageUrls],
|
146 |
+
outputs=[
|
147 |
+
worldcup,
|
148 |
+
battleImage1,
|
149 |
+
battleImage2,
|
150 |
+
startWorldcupBtn,
|
151 |
+
winImage1Btn,
|
152 |
+
winImage2Btn,
|
153 |
+
vsImage,
|
154 |
+
worldcupTitle,
|
155 |
+
],
|
156 |
+
)
|
157 |
+
def startWorldcupBtn(entryImageUrls):
|
158 |
+
print("startWorldcupBtn entryImageUrls:", entryImageUrls)
|
159 |
+
if len(entryImageUrls) in WORLDCUPABLE_IMAGE_CNT:
|
160 |
+
worldcup = Worldcup(images=entryImageUrls)
|
161 |
+
twoImages = worldcup.getCurrentRoundImages()
|
162 |
+
return (
|
163 |
+
worldcup,
|
164 |
+
gr.update(
|
165 |
+
value=twoImages[0],
|
166 |
+
visible=True,
|
167 |
+
elem_classes=BATTLE_IMAGE_INIT,
|
168 |
+
),
|
169 |
+
gr.update(
|
170 |
+
value=twoImages[1],
|
171 |
+
visible=True,
|
172 |
+
elem_classes=BATTLE_IMAGE_INIT,
|
173 |
+
),
|
174 |
+
gr.update(visible=False),
|
175 |
+
gr.update(visible=True),
|
176 |
+
gr.update(visible=True),
|
177 |
+
gr.update(visible=True),
|
178 |
+
worldcup.getKangRound(),
|
179 |
+
)
|
180 |
+
else:
|
181 |
+
gr.Warning(
|
182 |
+
"월드컵 참가 이미지 수는 4개, 8개 또는 16개여야 합니다.",
|
183 |
+
duration=5,
|
184 |
+
)
|
185 |
+
return gr.skip()
|
worldcup.py
ADDED
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
데이터 모델 정의 파일
|
3 |
+
"""
|
4 |
+
|
5 |
+
from dataclasses import dataclass
|
6 |
+
from config import FINAL_ROUND
|
7 |
+
|
8 |
+
|
9 |
+
@dataclass
|
10 |
+
class Worldcup:
|
11 |
+
"""
|
12 |
+
이미지 월드컵 이미지 리스트를 받아서 참가 이미지들 리스트와 다음 승리자 리스트(다음 라운드 출전 리스트)를 가지고 있는 클래스.
|
13 |
+
이 클래스는 다음 메소드들을 가지고 있음.
|
14 |
+
getCurrentRoundImages (다음 대전 이미지 2개를 리스트로 반환함.)
|
15 |
+
winImage (0 또는 1을 입력 받아 승리자 리스트에 추가하고 다음 라운드를 위해 index를 증가시킴.)
|
16 |
+
처음에는 8강 이미지 (8개) 가 주어지고, 이 이미지들 중 getCurrentRoundImages 를 호출해서 그 둘 중 이긴 이미지에 대한 피드백을 winImage 에 전달하고, 이긴 이미지를 승리자 리스트에 추가함.
|
17 |
+
이렇게 해서 8강, 4강, 결승전 이미지들을 반환하며 대전을 진행할 수 있는 클래스
|
18 |
+
|
19 |
+
사용 예시:
|
20 |
+
worldcup = Worldcup(images=이미지_리스트)
|
21 |
+
"""
|
22 |
+
|
23 |
+
images: list
|
24 |
+
winners: list = None
|
25 |
+
index: int = 0
|
26 |
+
|
27 |
+
def __post_init__(self):
|
28 |
+
if self.winners is None:
|
29 |
+
self.winners = []
|
30 |
+
|
31 |
+
def getCurrentRoundImages(self):
|
32 |
+
# 현재 라운드의 이미지가 모두 소진되었는지 확인
|
33 |
+
# 다음 대전할 이미지 2개 반환
|
34 |
+
return self.images[self.index : self.index + 2]
|
35 |
+
|
36 |
+
def winImage(self, winnerIndex):
|
37 |
+
print("winImage winnerIndex:", winnerIndex)
|
38 |
+
self.winners.append(self.images[self.index + winnerIndex])
|
39 |
+
self.index += 2
|
40 |
+
|
41 |
+
if self.getKangRound() == FINAL_ROUND:
|
42 |
+
return self.winners[0]
|
43 |
+
|
44 |
+
if self.index >= len(self.images):
|
45 |
+
print("winImage 다음 라운드로 진행", self.index, len(self.images))
|
46 |
+
# 다음 라운드로 진행: winners를 새로운 images로 설정
|
47 |
+
self.images = self.winners
|
48 |
+
self.winners = []
|
49 |
+
self.index = 0
|
50 |
+
|
51 |
+
# 현재 몇강인지 가져오는 함수
|
52 |
+
def getKang(self):
|
53 |
+
return len(self.images)
|
54 |
+
|
55 |
+
# 현재 라운드 가져오는 함수 (8강 2차전 할 때, 2에 해당 하는 값)
|
56 |
+
def getRound(self):
|
57 |
+
return self.index // 2 + 1
|
58 |
+
|
59 |
+
def getKangRound(self):
|
60 |
+
if self.getKang() == 2:
|
61 |
+
return FINAL_ROUND
|
62 |
+
return f"## <center>{self.getKang()}강 {self.getRound()}차전</center>"
|