Spaces:
Sleeping
Sleeping
Kimilhee
commited on
Commit
·
19d1c4b
1
Parent(s):
123f654
승리 이미지 선택할 때, 애니메이션 추가.
Browse files- .cursorrules +129 -0
- app.py +201 -23
- static/beauty/out-5.webp +0 -0
.cursorrules
ADDED
@@ -0,0 +1,129 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
You are an expert in Python project development, specializing in building well-structured, maintainable Python applications.
|
2 |
+
|
3 |
+
Core Expertise:
|
4 |
+
|
5 |
+
- Python Development
|
6 |
+
- Project Architecture
|
7 |
+
- Testing Strategies
|
8 |
+
- Code Quality
|
9 |
+
- Package Management
|
10 |
+
- Gradio
|
11 |
+
|
12 |
+
Development Guidelines:
|
13 |
+
|
14 |
+
1. Project Structure
|
15 |
+
ALWAYS:
|
16 |
+
|
17 |
+
- Use proper package layout
|
18 |
+
- Implement modular design
|
19 |
+
- Follow Python standards
|
20 |
+
- Use proper configuration
|
21 |
+
- Maintain documentation
|
22 |
+
|
23 |
+
NEVER:
|
24 |
+
|
25 |
+
- Mix package boundaries
|
26 |
+
- Skip project structure
|
27 |
+
- Ignore Python standards
|
28 |
+
- Use flat structure
|
29 |
+
|
30 |
+
2. Code Organization
|
31 |
+
ALWAYS:
|
32 |
+
|
33 |
+
- Use proper imports
|
34 |
+
- Implement clean architecture
|
35 |
+
- Follow SOLID principles
|
36 |
+
- Use type hints
|
37 |
+
- Document code properly
|
38 |
+
|
39 |
+
NEVER:
|
40 |
+
|
41 |
+
- Use circular imports
|
42 |
+
- Mix responsibilities
|
43 |
+
- Skip type annotations
|
44 |
+
- Ignore documentation
|
45 |
+
|
46 |
+
3. Dependency Management
|
47 |
+
ALWAYS:
|
48 |
+
|
49 |
+
- Use virtual environments
|
50 |
+
- Pin dependencies
|
51 |
+
- Use requirements files
|
52 |
+
- Handle dev dependencies
|
53 |
+
- Update regularly
|
54 |
+
|
55 |
+
NEVER:
|
56 |
+
|
57 |
+
- Mix environment dependencies
|
58 |
+
- Use global packages
|
59 |
+
- Skip version pinning
|
60 |
+
- Ignore security updates
|
61 |
+
|
62 |
+
4. Testing Strategy
|
63 |
+
ALWAYS:
|
64 |
+
|
65 |
+
- Write unit tests
|
66 |
+
- Implement integration tests
|
67 |
+
- Use proper fixtures
|
68 |
+
- Test edge cases
|
69 |
+
- Measure coverage
|
70 |
+
|
71 |
+
NEVER:
|
72 |
+
|
73 |
+
- Skip test documentation
|
74 |
+
- Mix test types
|
75 |
+
- Ignore test isolation
|
76 |
+
- Skip error scenarios
|
77 |
+
|
78 |
+
Code Quality:
|
79 |
+
|
80 |
+
- Use proper linting
|
81 |
+
- Implement formatting
|
82 |
+
- Follow style guides
|
83 |
+
- Use static analysis
|
84 |
+
- Monitor complexity
|
85 |
+
|
86 |
+
Documentation:
|
87 |
+
|
88 |
+
- Write clear docstrings
|
89 |
+
- Maintain README
|
90 |
+
- Document APIs
|
91 |
+
- Include examples
|
92 |
+
- Keep docs updated
|
93 |
+
|
94 |
+
Development Tools:
|
95 |
+
|
96 |
+
- Use proper IDE
|
97 |
+
- Configure debugger
|
98 |
+
- Use version control
|
99 |
+
- Implement CI/CD
|
100 |
+
- Use code analysis
|
101 |
+
|
102 |
+
Best Practices:
|
103 |
+
|
104 |
+
- Follow PEP standards
|
105 |
+
- Keep code clean
|
106 |
+
- Handle errors properly
|
107 |
+
- Use proper logging
|
108 |
+
- Implement monitoring
|
109 |
+
|
110 |
+
Package Distribution:
|
111 |
+
|
112 |
+
- Use proper packaging
|
113 |
+
- Handle versioning
|
114 |
+
- Write setup files
|
115 |
+
- Include metadata
|
116 |
+
- Document installation
|
117 |
+
|
118 |
+
Remember:
|
119 |
+
|
120 |
+
- Focus on maintainability
|
121 |
+
- Keep code organized
|
122 |
+
- Handle errors properly
|
123 |
+
- Document thoroughly
|
124 |
+
|
125 |
+
Gradio Integration:
|
126 |
+
|
127 |
+
- Create interactive demos using Gradio for model inference and visualization.
|
128 |
+
- Design user-friendly interfaces that showcase model capabilities.
|
129 |
+
- Implement proper error handling and input validation in Gradio apps.
|
app.py
CHANGED
@@ -4,6 +4,7 @@ from random import shuffle
|
|
4 |
import os
|
5 |
import gradio as gr
|
6 |
import replicate
|
|
|
7 |
|
8 |
# .env 파일 로드
|
9 |
load_dotenv()
|
@@ -354,8 +355,111 @@ footer {visibility: hidden}
|
|
354 |
.tiny-input {
|
355 |
width: '20px',
|
356 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
357 |
"""
|
358 |
|
|
|
|
|
359 |
# gr.set_static_paths(paths=["static/image/", "static/examples/"])
|
360 |
|
361 |
# theme = "NoCrypt/miku" # 하늘색 테마
|
@@ -491,6 +595,7 @@ def setup_demo():
|
|
491 |
interactive=False,
|
492 |
# height=768,
|
493 |
)
|
|
|
494 |
|
495 |
@entryListGallery.select(outputs=selectedImageIndex)
|
496 |
def setSelectedImageIndex(selection: gr.SelectData):
|
@@ -501,7 +606,11 @@ def setup_demo():
|
|
501 |
return gr.update(value=entryList), entryList
|
502 |
|
503 |
del entryList[selectedIndex]
|
504 |
-
return
|
|
|
|
|
|
|
|
|
505 |
|
506 |
# 왠지 모르게 마지막 요소를 지우면 preview=True 가 안되서 이렇게 함.
|
507 |
def reSelectedEntry(entryList):
|
@@ -510,7 +619,7 @@ def setup_demo():
|
|
510 |
removeEntryWorldcupBtn.click(
|
511 |
fn=removeSelectedEntry,
|
512 |
inputs=[entryImageUrls, selectedImageIndex],
|
513 |
-
outputs=[entryListGallery, entryImageUrls],
|
514 |
).then(
|
515 |
fn=reSelectedEntry,
|
516 |
inputs=entryImageUrls,
|
@@ -576,6 +685,10 @@ def setup_demo():
|
|
576 |
selectedImageIndex,
|
577 |
progressBar,
|
578 |
],
|
|
|
|
|
|
|
|
|
579 |
).then(
|
580 |
fn=lambda entryList: (
|
581 |
gr.Info(
|
@@ -586,7 +699,6 @@ def setup_demo():
|
|
586 |
else None
|
587 |
),
|
588 |
inputs=[entryImageUrls],
|
589 |
-
outputs=[],
|
590 |
)
|
591 |
|
592 |
with gr.Tab("이미지 월드컵"):
|
@@ -604,13 +716,35 @@ def setup_demo():
|
|
604 |
|
605 |
with gr.Row(): # 이미지 수�� 배열.
|
606 |
battleImage1 = gr.Image(
|
607 |
-
show_label=False,
|
|
|
|
|
|
|
|
|
608 |
)
|
609 |
vsImage = gr.HTML(VS_IMAGE)
|
610 |
battleImage2 = gr.Image(
|
611 |
-
show_label=False,
|
|
|
|
|
|
|
|
|
612 |
)
|
613 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
614 |
def winImage(winnerIndex, worldcup):
|
615 |
print("winnerIndex:", winnerIndex)
|
616 |
print("worldcup:", worldcup)
|
@@ -618,8 +752,14 @@ def setup_demo():
|
|
618 |
battleTitle = worldcup.getKangRound()
|
619 |
if finalWinner:
|
620 |
return (
|
621 |
-
gr.update(
|
622 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
623 |
worldcup,
|
624 |
"## <center>🎉 우승 🎊</center>",
|
625 |
gr.update(visible=False),
|
@@ -628,9 +768,16 @@ def setup_demo():
|
|
628 |
gr.update(visible=False),
|
629 |
)
|
630 |
nextMatchImages = worldcup.getCurrentRoundImages()
|
|
|
631 |
return (
|
632 |
-
|
633 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
634 |
worldcup,
|
635 |
battleTitle,
|
636 |
gr.skip(),
|
@@ -649,18 +796,41 @@ def setup_demo():
|
|
649 |
winImage1Btn,
|
650 |
winImage2Btn,
|
651 |
]
|
652 |
-
|
653 |
-
|
654 |
-
|
655 |
-
|
656 |
-
|
657 |
-
|
658 |
-
|
659 |
-
|
660 |
-
)
|
661 |
-
|
662 |
-
|
663 |
-
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
664 |
|
665 |
@startWorldcupBtn.click(
|
666 |
inputs=[entryImageUrls],
|
@@ -682,8 +852,16 @@ def setup_demo():
|
|
682 |
twoImages = worldcup.getCurrentRoundImages()
|
683 |
return (
|
684 |
worldcup,
|
685 |
-
gr.update(
|
686 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
687 |
gr.update(visible=False),
|
688 |
gr.update(visible=True),
|
689 |
gr.update(visible=True),
|
|
|
4 |
import os
|
5 |
import gradio as gr
|
6 |
import replicate
|
7 |
+
import time
|
8 |
|
9 |
# .env 파일 로드
|
10 |
load_dotenv()
|
|
|
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" # 하늘색 테마
|
|
|
595 |
interactive=False,
|
596 |
# height=768,
|
597 |
)
|
598 |
+
entryCount = gr.Markdown("## 이미지 월드컵 진출 이미지 수: 0")
|
599 |
|
600 |
@entryListGallery.select(outputs=selectedImageIndex)
|
601 |
def setSelectedImageIndex(selection: gr.SelectData):
|
|
|
606 |
return gr.update(value=entryList), entryList
|
607 |
|
608 |
del entryList[selectedIndex]
|
609 |
+
return (
|
610 |
+
entryList,
|
611 |
+
entryList,
|
612 |
+
f"## 이미지 월드컵 진출 이미지 수: {len(entryList)}",
|
613 |
+
)
|
614 |
|
615 |
# 왠지 모르게 마지막 요소를 지우면 preview=True 가 안되서 이렇게 함.
|
616 |
def reSelectedEntry(entryList):
|
|
|
619 |
removeEntryWorldcupBtn.click(
|
620 |
fn=removeSelectedEntry,
|
621 |
inputs=[entryImageUrls, selectedImageIndex],
|
622 |
+
outputs=[entryListGallery, entryImageUrls, entryCount],
|
623 |
).then(
|
624 |
fn=reSelectedEntry,
|
625 |
inputs=entryImageUrls,
|
|
|
685 |
selectedImageIndex,
|
686 |
progressBar,
|
687 |
],
|
688 |
+
).then(
|
689 |
+
fn=lambda entryImageUrls: f"## 이미지 월드컵 진출 이미지 수: {len(entryImageUrls)}",
|
690 |
+
inputs=entryImageUrls,
|
691 |
+
outputs=entryCount,
|
692 |
).then(
|
693 |
fn=lambda entryList: (
|
694 |
gr.Info(
|
|
|
699 |
else None
|
700 |
),
|
701 |
inputs=[entryImageUrls],
|
|
|
702 |
)
|
703 |
|
704 |
with gr.Tab("이미지 월드컵"):
|
|
|
716 |
|
717 |
with gr.Row(): # 이미지 수�� 배열.
|
718 |
battleImage1 = gr.Image(
|
719 |
+
show_label=False,
|
720 |
+
scale=5,
|
721 |
+
elem_classes="battle-image",
|
722 |
+
interactive=False,
|
723 |
+
show_download_button=False,
|
724 |
)
|
725 |
vsImage = gr.HTML(VS_IMAGE)
|
726 |
battleImage2 = gr.Image(
|
727 |
+
show_label=False,
|
728 |
+
scale=5,
|
729 |
+
elem_classes="battle-image",
|
730 |
+
interactive=False,
|
731 |
+
show_download_button=False,
|
732 |
)
|
733 |
|
734 |
+
def winImage0(winnerIndex):
|
735 |
+
print("winImage0 winnerIndex:", winnerIndex)
|
736 |
+
if winnerIndex == 0:
|
737 |
+
return gr.update(
|
738 |
+
elem_classes="fade-in non-clickable"
|
739 |
+
), gr.update(elem_classes="fade-out")
|
740 |
+
else:
|
741 |
+
return gr.update(elem_classes="fade-out"), gr.update(
|
742 |
+
elem_classes="fade-in non-clickable"
|
743 |
+
)
|
744 |
+
|
745 |
+
def sleep():
|
746 |
+
time.sleep(1)
|
747 |
+
|
748 |
def winImage(winnerIndex, worldcup):
|
749 |
print("winnerIndex:", winnerIndex)
|
750 |
print("worldcup:", worldcup)
|
|
|
752 |
battleTitle = worldcup.getKangRound()
|
753 |
if finalWinner:
|
754 |
return (
|
755 |
+
gr.update(
|
756 |
+
visible=winnerIndex == 0,
|
757 |
+
elem_classes=BATTLE_IMAGE_INIT,
|
758 |
+
),
|
759 |
+
gr.update(
|
760 |
+
visible=winnerIndex == 1,
|
761 |
+
elem_classes=BATTLE_IMAGE_INIT,
|
762 |
+
),
|
763 |
worldcup,
|
764 |
"## <center>🎉 우승 🎊</center>",
|
765 |
gr.update(visible=False),
|
|
|
768 |
gr.update(visible=False),
|
769 |
)
|
770 |
nextMatchImages = worldcup.getCurrentRoundImages()
|
771 |
+
print("nextMatchImages:", nextMatchImages)
|
772 |
return (
|
773 |
+
gr.update(
|
774 |
+
value=nextMatchImages[0],
|
775 |
+
elem_classes=BATTLE_IMAGE_INIT,
|
776 |
+
),
|
777 |
+
gr.update(
|
778 |
+
value=nextMatchImages[1],
|
779 |
+
elem_classes=BATTLE_IMAGE_INIT,
|
780 |
+
),
|
781 |
worldcup,
|
782 |
battleTitle,
|
783 |
gr.skip(),
|
|
|
796 |
winImage1Btn,
|
797 |
winImage2Btn,
|
798 |
]
|
799 |
+
|
800 |
+
# winImage1Btn_click = winImage1Btn.click
|
801 |
+
# winImage1Btn_click(
|
802 |
+
# fn=lambda w: winImage0(0),
|
803 |
+
# inputs=[battleImage1],
|
804 |
+
# outputs=[battleImage1, battleImage2],
|
805 |
+
# ).then(fn=sleep, inputs=[], outputs=[]).then(
|
806 |
+
# fn=lambda w: winImage(0, w), inputs=[worldcup], outputs=wOutputs
|
807 |
+
# )
|
808 |
+
# winImage2Btn.click(
|
809 |
+
# fn=lambda w: winImage(1, w), inputs=[worldcup], outputs=wOutputs
|
810 |
+
# )
|
811 |
+
# battleImage1.select(
|
812 |
+
# fn=lambda w: winImage(0, w), inputs=[worldcup], outputs=wOutputs
|
813 |
+
# )
|
814 |
+
# battleImage2.select(
|
815 |
+
# fn=lambda w: winImage(1, w), inputs=[worldcup], outputs=wOutputs
|
816 |
+
# )
|
817 |
+
|
818 |
+
for event, winIndex in [
|
819 |
+
(winImage1Btn.click, 0),
|
820 |
+
(winImage2Btn.click, 1),
|
821 |
+
(battleImage1.select, 0),
|
822 |
+
(battleImage2.select, 1),
|
823 |
+
]:
|
824 |
+
print("winIndex:", winIndex)
|
825 |
+
event(
|
826 |
+
fn=lambda w, i=winIndex: winImage0(i),
|
827 |
+
inputs=[battleImage1],
|
828 |
+
outputs=[battleImage1, battleImage2],
|
829 |
+
).then(fn=sleep).then(
|
830 |
+
fn=lambda w, i=winIndex: winImage(i, w),
|
831 |
+
inputs=[worldcup],
|
832 |
+
outputs=wOutputs,
|
833 |
+
)
|
834 |
|
835 |
@startWorldcupBtn.click(
|
836 |
inputs=[entryImageUrls],
|
|
|
852 |
twoImages = worldcup.getCurrentRoundImages()
|
853 |
return (
|
854 |
worldcup,
|
855 |
+
gr.update(
|
856 |
+
value=twoImages[0],
|
857 |
+
visible=True,
|
858 |
+
elem_classes=BATTLE_IMAGE_INIT,
|
859 |
+
),
|
860 |
+
gr.update(
|
861 |
+
value=twoImages[1],
|
862 |
+
visible=True,
|
863 |
+
elem_classes=BATTLE_IMAGE_INIT,
|
864 |
+
),
|
865 |
gr.update(visible=False),
|
866 |
gr.update(visible=True),
|
867 |
gr.update(visible=True),
|
static/beauty/out-5.webp
CHANGED
![]() |
![]() |