Dan Bochman commited on
Commit
cd45a7b
1 Parent(s): db691a4
.gitattributes CHANGED
@@ -2,3 +2,4 @@
2
  *.png filter=lfs diff=lfs merge=lfs -text
3
  *.jpeg filter=lfs diff=lfs merge=lfs -text
4
  *.webp filter=lfs diff=lfs merge=lfs -text
 
 
2
  *.png filter=lfs diff=lfs merge=lfs -text
3
  *.jpeg filter=lfs diff=lfs merge=lfs -text
4
  *.webp filter=lfs diff=lfs merge=lfs -text
5
+ *.gif filter=lfs diff=lfs merge=lfs -text
app.py CHANGED
@@ -1,191 +1,8 @@
1
- import asyncio
2
- import base64
3
- import logging
4
- import os
5
- import time
6
-
7
- import cv2
8
  import gradio as gr
9
- import httpx
10
- import numpy as np
11
- import requests
12
  from gradio.themes.utils import sizes
13
 
14
- # LOGGING
15
- logger = logging.getLogger("LookSwap")
16
- logger.setLevel(logging.INFO)
17
- handler = logging.StreamHandler()
18
- formatter = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s", datefmt="%Y-%m-%d %H:%M:%S")
19
- handler.setFormatter(formatter)
20
- logger.addHandler(handler)
21
-
22
- # IMAGE ASSETS
23
- ASSETS_DIR = os.path.join(os.path.dirname(__file__), "assets")
24
- WATERMARK = cv2.imread(os.path.join(ASSETS_DIR, "watermark.png"), cv2.IMREAD_UNCHANGED)
25
- WATERMARK = cv2.resize(WATERMARK, (0, 0), fx=0.5, fy=0.5, interpolation=cv2.INTER_AREA)
26
- NSFW = os.path.join(ASSETS_DIR, "nsfw.webp")
27
-
28
- # API CONFIG
29
- FASHN_API_URL = os.environ.get("FASHN_ENPOINT_URL")
30
- FASHN_API_KEY = os.environ.get("FASHN_API_KEY")
31
- assert FASHN_API_URL, "Please set the FASHN_ENPOINT_URL environment variable"
32
- assert FASHN_API_KEY, "Please set the FASHN_API_KEY environment variable"
33
-
34
- # ----------------- HELPER FUNCTIONS ----------------- #
35
-
36
-
37
- def add_watermark(image: np.array, watermark: np.array, offset: int = 5) -> np.array:
38
- """Adds a watermark to the image at the bottom right corner with a given offset."""
39
- image_height, image_width = image.shape[:2]
40
- watermark_height, watermark_width = watermark.shape[:2]
41
-
42
- # Calculate the position of the watermark in the bottom right corner, with a slight offset
43
- x_offset = image_width - watermark_width - offset
44
- y_offset = image_height - watermark_height - offset
45
-
46
- # Separate the watermark into its color and alpha channels
47
- overlay_color = watermark[:, :, :3]
48
- overlay_mask = watermark[:, :, 3]
49
-
50
- # Blend the watermark with the image
51
- for c in range(0, 3):
52
- image[y_offset : y_offset + watermark_height, x_offset : x_offset + watermark_width, c] = overlay_color[
53
- :, :, c
54
- ] * (overlay_mask / 255.0) + image[
55
- y_offset : y_offset + watermark_height, x_offset : x_offset + watermark_width, c
56
- ] * (
57
- 1.0 - overlay_mask / 255.0
58
- )
59
-
60
- return image
61
-
62
-
63
- def opencv_load_image_from_http(url: str) -> np.ndarray:
64
- """Loads an image from a given URL using HTTP GET."""
65
- with requests.get(url) as response:
66
- response.raise_for_status()
67
- image_data = np.frombuffer(response.content, np.uint8)
68
- image = cv2.imdecode(image_data, cv2.IMREAD_COLOR)
69
- return image
70
-
71
-
72
- def resize_image(img: np.array, short_axis_target: int = 512) -> np.array:
73
- """Resizes an image to keep the aspect ratio with the shortest axis not exceeding a target size."""
74
- height, width = img.shape[:2]
75
- scale_factor = short_axis_target / min(height, width)
76
- resized_img = cv2.resize(img, (0, 0), fx=scale_factor, fy=scale_factor, interpolation=cv2.INTER_AREA)
77
- return resized_img
78
-
79
-
80
- def encode_img_to_base64(img: np.array) -> str:
81
- """Encodes an image as a JPEG in Base64 format."""
82
- img = cv2.imencode(".jpg", img)[1].tobytes()
83
- img = base64.b64encode(img).decode("utf-8")
84
- img = f"data:image/jpeg;base64,{img}"
85
- return img
86
-
87
-
88
- def parse_checkboxes(checkboxes):
89
- checkboxes = [checkbox.lower().replace(" ", "_") for checkbox in checkboxes]
90
- checkboxes = {checkbox: True for checkbox in checkboxes}
91
- return checkboxes
92
-
93
-
94
- def verify_aspect_ratio(img: np.array, prefix: str = "Model"):
95
- height, width = img.shape[:2]
96
- aspect_ratio = width / height
97
- if aspect_ratio < 0.5:
98
- raise gr.Error(f"{prefix} image W:H aspect ratio is too low. Use 2:3 or 3:4 for best results.")
99
- elif aspect_ratio > 0.8:
100
- raise gr.Error(f"{prefix} image W:H aspect ratio is too high. Use 2:3 or 3:4 for best results.")
101
-
102
-
103
- # ----------------- CORE FUNCTION ----------------- #
104
-
105
- CATEGORY_API_MAPPING = {"Top": "tops", "Bottom": "bottoms", "Full-body": "one-pieces"}
106
-
107
-
108
- async def get_tryon_result(model_image, garment_image, category, model_checkboxes, request: gr.Request):
109
- logger.info("Starting new try-on request...")
110
-
111
- if request:
112
- client_ip = request.headers.get("x-forwarded-for") or request.client.host
113
-
114
- # verify aspect ratio of the input images
115
- verify_aspect_ratio(model_image, "Model")
116
- # verify_aspect_ratio(garment_image, "Garment")
117
-
118
- # preprocessing: convert to RGB, resize, encode to base64
119
- model_image, garment_image = map(lambda x: cv2.cvtColor(x, cv2.COLOR_RGB2BGR), [model_image, garment_image])
120
- model_image, garment_image = map(resize_image, [model_image, garment_image])
121
- model_image, garment_image = map(encode_img_to_base64, [model_image, garment_image])
122
- # prepare data for API request
123
- category = CATEGORY_API_MAPPING[category]
124
- data = {
125
- "model_image": model_image,
126
- "garment_image": garment_image,
127
- "category": category,
128
- **parse_checkboxes(model_checkboxes),
129
- }
130
- headers = {"Content-Type": "application/json", "Authorization": f"Bearer {FASHN_API_KEY}"}
131
-
132
- # make API request
133
- start_time = time.time()
134
- async with httpx.AsyncClient() as client:
135
- response = await client.post(f"{FASHN_API_URL}/run", headers=headers, json=data, timeout=httpx.Timeout(300.0))
136
- if response.is_error:
137
- raise gr.Error(f"API request failed: {response.text}")
138
-
139
- pred_id = response.json().get("id")
140
- logger.info(f"Prediction ID: {pred_id}")
141
-
142
- # poll the status of the prediction
143
- while True:
144
- current_time = time.time()
145
- elapsed_time = current_time - start_time
146
- if elapsed_time > 180: # 3 minutes
147
- raise gr.Error("Maximum polling time exceeded.")
148
-
149
- status_response = await client.get(
150
- f"{FASHN_API_URL}/status/{pred_id}", headers=headers, timeout=httpx.Timeout(10)
151
- )
152
- if status_response.is_error:
153
- raise Exception(f"Status polling failed: {status_response.text}")
154
-
155
- status_data = status_response.json()
156
- if status_data["status"] not in ["starting", "in_queue", "processing", "completed"]:
157
- error = status_data.get("error")
158
- error_msg = f"Prediction failed: {error}"
159
- if "NSFW" in error:
160
- if request:
161
- gr.Warning(f"NSFW attempt IP address: {client_ip}")
162
- return NSFW
163
- raise gr.Error(error_msg)
164
-
165
- logger.info(f"Prediction status: {status_data['status']}")
166
- if status_data["status"] == "completed":
167
- break
168
-
169
- await asyncio.sleep(3)
170
-
171
- # get the result image and add a watermark
172
- result_img = opencv_load_image_from_http(status_data["output"][0])
173
- result_img = cv2.cvtColor(result_img, cv2.COLOR_BGR2RGB)
174
- result_img = add_watermark(result_img, WATERMARK)
175
- return result_img
176
-
177
-
178
- # ----------------- GRADIO UI ----------------- #
179
-
180
-
181
  with open("banner.html", "r") as file:
182
  banner = file.read()
183
- with open("tips.html", "r") as file:
184
- tips = file.read()
185
- with open("footer.html", "r") as file:
186
- footer = file.read()
187
- with open("docs.html", "r") as file:
188
- docs = file.read()
189
 
190
  CUSTOM_CSS = """
191
  .image-container img {
@@ -198,50 +15,6 @@ CUSTOM_CSS = """
198
 
199
  with gr.Blocks(css=CUSTOM_CSS, theme=gr.themes.Monochrome(radius_size=sizes.radius_md)) as demo:
200
  gr.HTML(banner)
201
- gr.HTML(tips)
202
- with gr.Row():
203
- with gr.Column():
204
- model_image = gr.Image(label="Model Image", type="numpy", format="png")
205
- # create a checkbox to toggle "remove accessories"
206
- model_checkboxes = gr.CheckboxGroup(
207
- choices=["Remove Accessories", "Restore Hands", "Cover Feet"], label="Additional Controls", type="value"
208
- )
209
- example_model = gr.Examples(
210
- inputs=model_image,
211
- examples_per_page=10,
212
- examples=[
213
- os.path.join(ASSETS_DIR, "models", img) for img in os.listdir(os.path.join(ASSETS_DIR, "models"))
214
- ],
215
- )
216
- with gr.Column():
217
- garment_image = gr.Image(label="Garment Image", type="numpy", format="png")
218
- category = gr.Radio(choices=["Top", "Bottom", "Full-body"], label="Select Category", value="Top")
219
-
220
- example_garment = gr.Examples(
221
- inputs=garment_image,
222
- examples_per_page=10,
223
- examples=[
224
- os.path.join(ASSETS_DIR, "garments", img)
225
- for img in os.listdir(os.path.join(ASSETS_DIR, "garments"))
226
- ],
227
- )
228
-
229
- with gr.Column():
230
- result_image = gr.Image(label="Try-on Result", format="png")
231
- run_button = gr.Button("Run")
232
-
233
- gr.HTML(docs)
234
-
235
- run_button.click(
236
- fn=get_tryon_result,
237
- inputs=[model_image, garment_image, category, model_checkboxes],
238
- outputs=[result_image],
239
- )
240
-
241
- gr.HTML(footer)
242
-
243
 
244
  if __name__ == "__main__":
245
- ip = requests.get("http://ifconfig.me/ip", timeout=1).text.strip()
246
- logger.info(f"VM IP address: {ip}")
247
- demo.launch(share=False)
 
 
 
 
 
 
 
 
1
  import gradio as gr
 
 
 
2
  from gradio.themes.utils import sizes
3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
  with open("banner.html", "r") as file:
5
  banner = file.read()
 
 
 
 
 
 
6
 
7
  CUSTOM_CSS = """
8
  .image-container img {
 
15
 
16
  with gr.Blocks(css=CUSTOM_CSS, theme=gr.themes.Monochrome(radius_size=sizes.radius_md)) as demo:
17
  gr.HTML(banner)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
 
19
  if __name__ == "__main__":
20
+ demo.launch(show_api=False)
 
 
assets/fashn_logo.png DELETED

Git LFS Details

  • SHA256: ea3d0d1f0e3c57402aea45f719658d6eeeac381a3226e40824f79c01d7ed007c
  • Pointer size: 129 Bytes
  • Size of remote file: 4.12 kB
assets/garments/fendi_top.png DELETED

Git LFS Details

  • SHA256: d2ad26fb1ec7d7f042db9a4df425198ecc110ce8d7cbd78a5a61da9c33a59162
  • Pointer size: 131 Bytes
  • Size of remote file: 331 kB
assets/garments/jennie_chanel_dress.png DELETED

Git LFS Details

  • SHA256: 401d4800aa0d857111c558aa83326f538baac5d5d73367b957f66f33e19eff00
  • Pointer size: 131 Bytes
  • Size of remote file: 381 kB
assets/garments/pexels_flower_tank_top.jpg DELETED

Git LFS Details

  • SHA256: 1a2f3d3588cd2631f46da829584d6a2c75c6b6683960b2f074a3e5d94012749b
  • Pointer size: 132 Bytes
  • Size of remote file: 1.62 MB
assets/garments/prada_top.png DELETED

Git LFS Details

  • SHA256: 9b243addd1d0f3c8eb724249394279e7c0f7deebfbf12d77bd94efbe3c40d6cf
  • Pointer size: 131 Bytes
  • Size of remote file: 413 kB
assets/garments/ryan_gosling_long_sleeve_shirt.jpeg DELETED

Git LFS Details

  • SHA256: bcb7e28e246d888a3c5ed5d5881cf6a91022f46c5de4ca8bab28ede7ee070c02
  • Pointer size: 130 Bytes
  • Size of remote file: 87.5 kB
assets/garments/ryan_gosling_mickey_mouse.jpg DELETED

Git LFS Details

  • SHA256: 826c854c7d75c8cd0dc57fb17c563e6b2b6b93354554e31c933fb96239eca683
  • Pointer size: 130 Bytes
  • Size of remote file: 53.2 kB
assets/garments/taylor_swift_full.jpeg DELETED

Git LFS Details

  • SHA256: 1c40bccd488b6cbc0f8def37d484001b597a337c2b10dcf6162d9393296bd392
  • Pointer size: 131 Bytes
  • Size of remote file: 152 kB
assets/garments/uniqlo_mofusand.jpg DELETED

Git LFS Details

  • SHA256: 1477e9708cbdef7395954dbc524835f322bac17590857ad2017efe27bbfd75a3
  • Pointer size: 131 Bytes
  • Size of remote file: 321 kB
assets/garments/urban_oufitters_utah.webp DELETED

Git LFS Details

  • SHA256: 887edc9550c304f56ae2274f476a467c44db0ca2dbf71d8a9b7899c34cacc306
  • Pointer size: 131 Bytes
  • Size of remote file: 318 kB
assets/garments/yeji_checkered_pants.jpg DELETED

Git LFS Details

  • SHA256: f35a001f75a6645adfaa43a4eea8583d3d362be8690235ba458b4fe13acd0d2f
  • Pointer size: 131 Bytes
  • Size of remote file: 103 kB
assets/models/0_fashionnova.webp DELETED

Git LFS Details

  • SHA256: 21e07bd2a83bbe3bf3225f48e4362cecb7f7a232967e1393a4e38e18e6610a13
  • Pointer size: 131 Bytes
  • Size of remote file: 161 kB
assets/models/emery_rose.jpg DELETED

Git LFS Details

  • SHA256: 1036622e3acb27a974538738f1fb0c7e91d6971871aed239575cf609b35f81d1
  • Pointer size: 130 Bytes
  • Size of remote file: 52.6 kB
assets/models/emrata.jpg DELETED

Git LFS Details

  • SHA256: 4719512a5770446023bd9cb1f98217b3630b847619c6ff3ea493a5f75003c05b
  • Pointer size: 131 Bytes
  • Size of remote file: 123 kB
assets/models/idris_elba.jpg DELETED

Git LFS Details

  • SHA256: 76ace888ccf64a3c47bd6e773403ea3271411d4eecc5b36ac4116ce41ddfea2f
  • Pointer size: 131 Bytes
  • Size of remote file: 109 kB
assets/models/kylie.png DELETED

Git LFS Details

  • SHA256: aa7910676dadfcbfecfecc39cea914a896af895b19c313616b0da04979d3d1c2
  • Pointer size: 132 Bytes
  • Size of remote file: 1.02 MB
assets/models/lisa.jpeg DELETED

Git LFS Details

  • SHA256: daf28f7e931d4a7f75aa41a55723f5a284083847a1f29c19cbc72756f75904e1
  • Pointer size: 130 Bytes
  • Size of remote file: 92.4 kB
assets/models/pexels.jpg DELETED

Git LFS Details

  • SHA256: fe47751c32d9c634bdc8b9f4ce783f393bbf30734361328e4289317862ccf396
  • Pointer size: 132 Bytes
  • Size of remote file: 1.03 MB
assets/models/pexels_leg_on_wall.jpg DELETED

Git LFS Details

  • SHA256: 2e4f29765535c481659a86cef12912bca1f90c1bd5a409177efed78933d57fab
  • Pointer size: 132 Bytes
  • Size of remote file: 2.87 MB
assets/models/ryan_gosling.jpeg DELETED

Git LFS Details

  • SHA256: 858d500106a4a125e51df47ac2d3a9af9fce3b10bd514d5c83a5826587646f82
  • Pointer size: 131 Bytes
  • Size of remote file: 107 kB
assets/models/winona_ryder.jpg DELETED

Git LFS Details

  • SHA256: d4fcd6d6fd8c14486756bab468fd0c62b52dcd4f37b20bd41a9d83f0c40c9390
  • Pointer size: 130 Bytes
  • Size of remote file: 90.5 kB
assets/nsfw.webp DELETED

Git LFS Details

  • SHA256: 73cea9cd34d0fe6c160beed45212805862cc95b78e0d9576907a09fd74625d5f
  • Pointer size: 132 Bytes
  • Size of remote file: 4.82 MB
assets/watermark.png DELETED

Git LFS Details

  • SHA256: 737e9a6c2731a049e9da951a6aade6effa9167e0a069c81d294777ed2123260f
  • Pointer size: 129 Bytes
  • Size of remote file: 2.8 kB
banner.html CHANGED
@@ -22,8 +22,6 @@
22
  ">
23
  FASHN AI: LookSwap
24
  </h1>
25
- <p style="font-size: 14px; margin: 0; color: #fafafa;
26
- ">v0.6</p>
27
 
28
 
29
  </div>
@@ -35,9 +33,12 @@
35
  color: #fafafa;
36
  opacity: 0.8;
37
  ">
38
- Welcome to the official FASHN AI demo! Using our dashboard or API, you can style any
39
- model with either tops, bottoms or full outfits. <br />Our algorithm specializes in
40
- swapping clothes from one person to another.
 
 
 
41
  </p>
42
 
43
  <div style="
@@ -68,4 +69,6 @@
68
  alt="Discord" />
69
  </a>
70
  </div>
 
 
71
  </div>
 
22
  ">
23
  FASHN AI: LookSwap
24
  </h1>
 
 
25
 
26
 
27
  </div>
 
33
  color: #fafafa;
34
  opacity: 0.8;
35
  ">
36
+
37
+ 🚚 &nbsp;<b style="color: #fafafa;">WE ARE MOVING</b>&nbsp; 📦 <br>
38
+ Many thanks to everyone who has liked this space and given us feedback. We have decided to stop maintaining this
39
+ space. <br>
40
+ You can still try LookSwap for free on our <a href="https://fashn.ai/" target="_blank"
41
+ style="color:#fafafa">website</a>.
42
  </p>
43
 
44
  <div style="
 
69
  alt="Discord" />
70
  </a>
71
  </div>
72
+ <img src="https://media.tenor.com/gHygBs_JkKwAAAAi/moving-boxes.gif" alt="Moving Boxes"
73
+ style="width: 100%; max-width: 200px; margin-top: 24px;" />
74
  </div>
docs.html DELETED
@@ -1,27 +0,0 @@
1
- <div style="
2
- padding: 12px;
3
- border: 1px solid #333333;
4
- border-radius: 8px;
5
- text-align: left;
6
- display: flex;
7
- flex-direction: column;
8
- gap: 8px;
9
- ">
10
- <ul style="
11
- display: flex;
12
- flex-direction: column;
13
- gap: 12px;
14
- justify-content: left;
15
- align-items: left;
16
- padding: 0;
17
- list-style: none;
18
- font-size: 14px;
19
- ">
20
- <li><strong>Category</strong> - Determines which part of the oufit to take from the garment image.</li>
21
- <li><strong>Remove Accessories</strong> - Strips items such as belts, neckties, and handbags from the model.</li>
22
- <li><strong>Restore Hands</strong> - Preserves the model's hands better, but may adjust sleeve length.
23
- </li>
24
- <li><strong>Cover Feet</strong> - Use when garment should cover the feet (e.g., mermaid dress) or to swap out the
25
- model’s shoes.</li>
26
- </ul>
27
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
footer.html DELETED
@@ -1,54 +0,0 @@
1
- <div style="
2
- display: flex;
3
- flex-direction: column;
4
- background: linear-gradient(45deg, #1a1a1a 0%, #333333 100%);
5
- padding: 24px;
6
- gap: 24px;
7
- border-radius: 8px;
8
- align-items: center;
9
- ">
10
- <div style="display: flex; justify-content: center; gap: 8px;">
11
- <h1 style="
12
- font-size: 24px;
13
- color: #fafafa;
14
- margin: 0;
15
- font-family: 'Trebuchet MS', 'Lucida Sans Unicode', 'Lucida Grande',
16
- 'Lucida Sans', Arial, sans-serif;
17
- ">
18
- FAQ
19
- </h1>
20
- </div>
21
- <div style="max-width: 790px; text-align: center; display: flex; flex-direction: column; gap: 12px;">
22
- <div>
23
- <div style="text-align: center;">
24
- <strong style="color: #fafafa; font-size: 16px;">What is this model?</strong>
25
- </div>
26
- <p style="
27
- /* margin: 0; */
28
- line-height: 1.6rem;
29
- font-size: 16px;
30
- color: #fafafa;
31
- opacity: 0.8;
32
- ">
33
- LookSwap is a custom 1.2B parameter diffusion architecture, designed specifically for the task of transferring
34
- clothes between images. This model was trained on an internal dataset of 2M images for about 2000 A100 GPU hours
35
- (sorry 🌎).
36
- </p>
37
- </div>
38
-
39
- <div>
40
- <div style="text-align: center;">
41
- <strong style="color: #fafafa; font-size: 16px;">Can you support higher resolutions?</strong>
42
- </div>
43
- <p style="
44
- /* margin: 0; */
45
- line-height: 1.6rem;
46
- font-size: 16px;
47
- color: #fafafa;
48
- opacity: 0.8;
49
- ">
50
- Yes. Image generation at 384x576 resolution is available on the FASHN platform.
51
- </p>
52
- </div>
53
- </div>
54
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
requirements.txt CHANGED
@@ -1,4 +1 @@
1
- gradio
2
- numpy
3
- requests
4
- opencv-python
 
1
+ gradio
 
 
 
tips.html DELETED
@@ -1,28 +0,0 @@
1
- <div style="
2
- padding: 12px;
3
- border: 1px solid #333333;
4
- border-radius: 8px;
5
- text-align: center;
6
- display: flex;
7
- flex-direction: column;
8
- gap: 8px;
9
- ">
10
- <b style="font-size: 18px;"> ❣️ Tips for successful try-on generations</b>
11
-
12
- <ul style="
13
- display: flex;
14
- gap: 12px;
15
- justify-content: center;
16
- li {
17
- margin: 0;
18
- }
19
- ">
20
- <li>2:3 aspect ratio</li>
21
- <li>One person per image </li>
22
- <li>Focus/Zoom on subject</li>
23
- <li>Similar poses between images</li>
24
- <li>Simple backgrounds</li>
25
- <li>High-quality images</li>
26
- <li>Take inspiration from the examples below</li>
27
- </ul>
28
- </div>