Kimilhee commited on
Commit
db315f2
·
1 Parent(s): ed8900a

파일 분리 리팩토링.

Browse files
README.md CHANGED
@@ -1,5 +1,5 @@
1
  ---
2
- title: Dk Image Worldcup
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: DK Image Content Generation Service
11
  ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
14
 
15
- ### Run the server
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- BATTLE_IMAGE_INIT = "battle-image fade-in-visibility"
 
 
462
 
463
- # gr.set_static_paths(paths=["static/image/", "static/examples/"])
 
 
 
464
 
465
- # theme = "NoCrypt/miku" # 하늘색 테마
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
- # VS_IMAGE = "<img src='/gradio_api/file=static/image/vs.png' style='margin-top: 100px;'>"
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("# ![대교로고](/gradio_api/file=static/image/daekyo_logo_kr_white.png) 대교 콘텐츠 이미지 생성기")
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
- def show_progress():
658
- return gr.update(label="이미지 생성 중", visible=True)
659
 
660
- def hide_progress():
661
- return gr.update(visible=False)
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
- # hot reload 하려면 demo는 전역변수로 해야 함. (변수명도 demo 고정!)
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.8.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.17.0
11
- fsspec==2025.2.0
12
- gradio==5.20.0
13
- gradio_client==1.7.2
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.1
19
  idna==3.10
20
- Jinja2==3.1.5
21
  markdown-it-py==3.0.0
22
  MarkupSafe==2.1.5
23
  mdurl==0.1.2
24
- numpy==2.2.3
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.9.9
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.0
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>"