Kims12 commited on
Commit
1e9bbd5
ยท
verified ยท
1 Parent(s): 45f2341

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +102 -105
app.py CHANGED
@@ -8,6 +8,7 @@ import gradio as gr
8
  import base64
9
  import mimetypes
10
  import logging
 
11
 
12
  from google import genai
13
  from google.genai import types
@@ -29,9 +30,9 @@ def save_binary_file(file_name, data):
29
  logger.debug(f"ํŒŒ์ผ ์ €์žฅ ์™„๋ฃŒ: {file_name}")
30
 
31
 
32
- def generate(text, file_name, background_file=None, style_file=None, model="gemini-2.0-flash-exp-image-generation"):
33
- logger.debug(f"generate ํ•จ์ˆ˜ ์‹œ์ž‘ - ํ…์ŠคํŠธ: '{text}', ํŒŒ์ผ๋ช…: '{file_name}', ๋ชจ๋ธ: '{model}'")
34
-
35
  try:
36
  # API ํ‚ค๋Š” ํ™˜๊ฒฝ๋ณ€์ˆ˜์—์„œ ๋ถˆ๋Ÿฌ์˜ด
37
  effective_api_key = os.environ.get("GEMINI_API_KEY")
@@ -44,68 +45,43 @@ def generate(text, file_name, background_file=None, style_file=None, model="gemi
44
  client = genai.Client(api_key=effective_api_key)
45
  logger.debug("Gemini ํด๋ผ์ด์–ธํŠธ ์ดˆ๊ธฐํ™” ์™„๋ฃŒ.")
46
 
47
- # ์›๋ณธ, ๋ฐฐ๊ฒฝ, ์Šคํƒ€์ผ ์ด๋ฏธ์ง€๋ฅผ ๊ฐ๊ฐ ์—…๋กœ๋“œ (์กด์žฌํ•˜๋Š” ๊ฒฝ์šฐ)
48
- uploaded_files = []
49
- uploaded_files.append(client.files.upload(file=file_name))
50
- logger.debug(f"์›๋ณธ ํŒŒ์ผ ์—…๋กœ๋“œ ์™„๋ฃŒ. URI: {uploaded_files[0].uri}, MIME ํƒ€์ž…: {uploaded_files[0].mime_type}")
51
-
52
- if background_file is not None:
53
- bg_file = client.files.upload(file=background_file)
54
- uploaded_files.append(bg_file)
55
- logger.debug(f"๋ฐฐ๊ฒฝ ํŒŒ์ผ ์—…๋กœ๋“œ ์™„๋ฃŒ. URI: {bg_file.uri}, MIME ํƒ€์ž…: {bg_file.mime_type}")
56
- if style_file is not None:
57
- style_uploaded = client.files.upload(file=style_file)
58
- uploaded_files.append(style_uploaded)
59
- logger.debug(f"์Šคํƒ€์ผ ํŒŒ์ผ ์—…๋กœ๋“œ ์™„๋ฃŒ. URI: {style_uploaded.uri}, MIME ํƒ€์ž…: {style_uploaded.mime_type}")
60
-
61
- # ์ปจํ…์ธ  ๊ฐ์ฒด ์ƒ์„ฑ: ์—…๋กœ๋“œํ•œ ์ด๋ฏธ์ง€๋“ค์„ ์ˆœ์„œ๋Œ€๋กœ(์›๋ณธ, ๋ฐฐ๊ฒฝ, ์Šคํƒ€์ผ) ์ถ”๊ฐ€ํ•˜๊ณ  ๋งˆ์ง€๋ง‰์— ํ…์ŠคํŠธ ํ”„๋กฌํ”„ํŠธ ์ถ”๊ฐ€
62
- parts = []
63
- # ์›๋ณธ ์ด๋ฏธ์ง€ ํŒŒํŠธ
64
- parts.append(
65
- types.Part.from_uri(
66
- file_uri=uploaded_files[0].uri,
67
- mime_type=uploaded_files[0].mime_type,
68
- )
69
- )
70
- # ๋ฐฐ๊ฒฝ ์ด๋ฏธ์ง€ ํŒŒํŠธ (์กด์žฌ ์‹œ)
71
- if background_file is not None:
72
- parts.append(
73
- types.Part.from_uri(
74
- file_uri=uploaded_files[1].uri,
75
- mime_type=uploaded_files[1].mime_type,
76
- )
77
- )
78
- # ์Šคํƒ€์ผ ์ด๋ฏธ์ง€ ํŒŒํŠธ (์กด์žฌ ์‹œ)
79
- if style_file is not None:
80
- # ๋ฐฐ๊ฒฝ์ด ์—†๋Š” ๊ฒฝ์šฐ uploaded_files[1]๊ฐ€ ์Šคํƒ€์ผ ์ด๋ฏธ์ง€๊ฐ€ ๋จ์— ์ฃผ์˜
81
- style_index = 2 if background_file is not None else 1
82
- parts.append(
83
- types.Part.from_uri(
84
- file_uri=uploaded_files[style_index].uri,
85
- mime_type=uploaded_files[style_index].mime_type,
86
- )
87
- )
88
- # ๋งˆ์ง€๋ง‰์œผ๋กœ ํ…์ŠคํŠธ ํŒŒํŠธ ์ถ”๊ฐ€
89
- parts.append(types.Part.from_text(text=text))
90
-
91
- contents = [
92
- types.Content(
93
- role="user",
94
- parts=parts,
95
- ),
96
- ]
97
- logger.debug(f"์ปจํ…์ธ  ๊ฐ์ฒด ์ƒ์„ฑ ์™„๋ฃŒ: {contents}")
98
-
99
  generate_content_config = types.GenerateContentConfig(
100
  temperature=1,
101
  top_p=0.95,
102
  top_k=40,
103
  max_output_tokens=8192,
104
- response_modalities=[
105
- "image",
106
- "text",
107
- ],
108
- response_mime_type="text/plain",
109
  )
110
  logger.debug(f"์ƒ์„ฑ ์„ค์ •: {generate_content_config}")
111
 
@@ -113,30 +89,31 @@ def generate(text, file_name, background_file=None, style_file=None, model="gemi
113
  temp_path = tmp.name
114
  logger.debug(f"์ž„์‹œ ํŒŒ์ผ ์ƒ์„ฑ๋จ: {temp_path}")
115
 
116
- response_stream = client.models.generate_content_stream(
 
117
  model=model,
118
  contents=contents,
119
  config=generate_content_config,
120
  )
121
-
122
- logger.debug("์‘๋‹ต ์ŠคํŠธ๋ฆผ ์ฒ˜๋ฆฌ ์‹œ์ž‘...")
123
- for chunk in response_stream:
124
- if not chunk.candidates or not chunk.candidates[0].content or not chunk.candidates[0].content.parts:
125
- logger.warning("chunk์— ํ›„๋ณด, ์ปจํ…์ธ , ๋˜๋Š” ํŒŒํŠธ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. ๊ฑด๋„ˆ๋œ๋‹ˆ๋‹ค.")
126
- continue
127
-
128
- inline_data = chunk.candidates[0].content.parts[0].inline_data
129
- if inline_data:
130
- save_binary_file(temp_path, inline_data.data)
131
- logger.info(f"MIME ํƒ€์ž… {inline_data.mime_type}์˜ ํŒŒ์ผ์ด ์ €์žฅ๋จ: {temp_path} (ํ”„๋กฌํ”„ํŠธ: {text})")
132
- else:
133
- logger.info(f"์ˆ˜์‹ ๋œ ํ…์ŠคํŠธ: {chunk.text}")
134
- print(chunk.text)
135
-
136
- logger.debug(f"Raw chunk: {chunk}")
137
-
138
- del uploaded_files
139
- logger.debug("์—…๋กœ๋“œ๋œ ํŒŒ์ผ ์ •๋ณด ์‚ญ์ œ ์™„๋ฃŒ.")
140
  return temp_path
141
 
142
  except Exception as e:
@@ -169,20 +146,41 @@ def process_image_and_prompt(original_pil, prompt, background_pil=None, style_pi
169
  style_pil.save(style_path)
170
  logger.debug(f"์Šคํƒ€์ผ ์ด๋ฏธ์ง€ ์ €์žฅ ์™„๋ฃŒ: {style_path}")
171
 
172
- input_text = prompt
 
 
 
 
 
 
 
 
173
  model = "gemini-2.0-flash-exp-image-generation"
174
 
175
- gemma_edited_image_path = generate(text=input_text,
176
- file_name=original_path,
177
- background_file=background_path,
178
- style_file=style_path,
179
- model=model)
 
 
180
 
181
  if gemma_edited_image_path:
182
  logger.debug(f"์ด๋ฏธ์ง€ ์ƒ์„ฑ ์™„๋ฃŒ. ๊ฒฝ๋กœ: {gemma_edited_image_path}")
183
  result_img = Image.open(gemma_edited_image_path)
184
  if result_img.mode == "RGBA":
185
  result_img = result_img.convert("RGB")
 
 
 
 
 
 
 
 
 
 
 
186
  return [result_img]
187
  else:
188
  logger.error("generate ํ•จ์ˆ˜์—์„œ None ๋ฐ˜ํ™˜๋จ.")
@@ -212,9 +210,9 @@ with gr.Blocks() as demo:
212
 
213
  with gr.Row():
214
  with gr.Column():
215
- original_input = gr.Image(type="pil", label="์›๋ณธ ์ด๋ฏธ์ง€", image_mode="RGBA")
216
- background_input = gr.Image(type="pil", label="๋ฐฐ๊ฒฝ ์ด๋ฏธ์ง€", image_mode="RGBA")
217
- style_input = gr.Image(type="pil", label="์Šคํƒ€์ผ ์ด๋ฏธ์ง€", image_mode="RGBA")
218
  prompt_input = gr.Textbox(
219
  lines=2,
220
  placeholder="ํŽธ์ง‘ํ•  ๋‚ด์šฉ์„ ์ž…๋ ฅํ•˜์„ธ์š”...",
@@ -223,6 +221,7 @@ with gr.Blocks() as demo:
223
  submit_btn = gr.Button("์ด๋ฏธ์ง€ ํŽธ์ง‘ ์‹คํ–‰")
224
  with gr.Column():
225
  output_gallery = gr.Gallery(label="ํŽธ์ง‘ ๊ฒฐ๊ณผ")
 
226
 
227
  submit_btn.click(
228
  fn=process_image_and_prompt,
@@ -230,19 +229,17 @@ with gr.Blocks() as demo:
230
  outputs=output_gallery,
231
  )
232
 
233
- # --- ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ---
234
- # ํ…Œ์ŠคํŠธ์šฉ ๋”๋ฏธ ์ด๋ฏธ์ง€ (์‹ค์ œ ์ด๋ฏธ์ง€๋กœ ๋Œ€์ฒด ๊ฐ€๋Šฅ)
235
- dummy_original = Image.new("RGBA", (100, 100), color="red")
236
- dummy_background = Image.new("RGBA", (100, 100), color="green")
237
- dummy_style = Image.new("RGBA", (100, 100), color="blue")
238
- dummy_prompt = "์ด๋ฏธ์ง€ ํŽธ์ง‘: ์›๋ณธ์€ ๋นจ๊ฐ•, ๋ฐฐ๊ฒฝ์€ ์ดˆ๋ก, ์Šคํƒ€์ผ์€ ํŒŒ๋ž‘"
239
-
240
- logger.info("process_image_and_prompt ํ•จ์ˆ˜๋ฅผ ์ง์ ‘ ํ˜ธ์ถœํ•ฉ๋‹ˆ๋‹ค...")
241
- result = process_image_and_prompt(dummy_original, dummy_prompt, dummy_background, dummy_style)
242
-
243
- if result:
244
- logger.info(f"์ง์ ‘ ํ˜ธ์ถœ ์„ฑ๊ณต. ๊ฒฐ๊ณผ: {result}")
245
- else:
246
- logger.error("์ง์ ‘ ํ˜ธ์ถœ ์‹คํŒจ.")
247
-
248
- demo.launch(share=True)
 
8
  import base64
9
  import mimetypes
10
  import logging
11
+ from io import BytesIO
12
 
13
  from google import genai
14
  from google.genai import types
 
30
  logger.debug(f"ํŒŒ์ผ ์ €์žฅ ์™„๋ฃŒ: {file_name}")
31
 
32
 
33
+ def generate(text, original_image_path, background_image_path=None, style_image_path=None, model="gemini-2.0-flash-exp-image-generation"):
34
+ logger.debug(f"generate ํ•จ์ˆ˜ ์‹œ์ž‘ - ํ…์ŠคํŠธ: '{text}', ์›๋ณธ ํŒŒ์ผ๋ช…: '{original_image_path}', ๋ชจ๋ธ: '{model}'")
35
+
36
  try:
37
  # API ํ‚ค๋Š” ํ™˜๊ฒฝ๋ณ€์ˆ˜์—์„œ ๋ถˆ๋Ÿฌ์˜ด
38
  effective_api_key = os.environ.get("GEMINI_API_KEY")
 
45
  client = genai.Client(api_key=effective_api_key)
46
  logger.debug("Gemini ํด๋ผ์ด์–ธํŠธ ์ดˆ๊ธฐํ™” ์™„๋ฃŒ.")
47
 
48
+ # PIL ์ด๋ฏธ์ง€ ๊ฐ์ฒด๋กœ ๋ณ€ํ™˜
49
+ original_img = Image.open(original_image_path)
50
+
51
+ # ์ปจํ…์ธ  ๋ฆฌ์ŠคํŠธ ์ƒ์„ฑ (๊ณต์‹ ๋ฌธ์„œ ๋ฐฉ์‹๋Œ€๋กœ)
52
+ contents = []
53
+
54
+ # ํ…์ŠคํŠธ ํ”„๋กฌํ”„ํŠธ๋ฅผ ๋ช…ํ™•ํ•˜๊ฒŒ ์ž‘์„ฑ
55
+ prompt = text
56
+ if background_image_path and "๋ฐฐ๊ฒฝ" not in text.lower():
57
+ prompt += " ์›๋ณธ ์ด๋ฏธ์ง€์˜ ๋ฐฐ๊ฒฝ์„ ๋‘ ๋ฒˆ์งธ ์—…๋กœ๋“œ๋œ ์ด๋ฏธ์ง€๋กœ ์™„์ „ํžˆ ๊ต์ฒดํ•ด ์ฃผ์„ธ์š”. ์ด๋ฏธ์ง€๋ฅผ ์—…๋ฐ์ดํŠธํ•˜๊ณ  ๊ฒฐ๊ณผ๋ฅผ ๋ณด์—ฌ์ฃผ์„ธ์š”."
58
+ if style_image_path and "์Šคํƒ€์ผ" not in text.lower():
59
+ prompt += " ์„ธ ๋ฒˆ์งธ ์ด๋ฏธ์ง€์˜ ์Šคํƒ€์ผ์„ ์ „์ฒด์ ์œผ๋กœ ์ ์šฉํ•ด ์ฃผ์„ธ์š”."
60
+
61
+ contents.append(prompt)
62
+ contents.append(original_img)
63
+
64
+ # ๋ฐฐ๊ฒฝ ์ด๋ฏธ์ง€ ์ถ”๊ฐ€ (์žˆ๋Š” ๊ฒฝ์šฐ)
65
+ if background_image_path:
66
+ background_img = Image.open(background_image_path)
67
+ contents.append(background_img)
68
+ logger.debug("๋ฐฐ๊ฒฝ ์ด๋ฏธ์ง€ ์ถ”๊ฐ€๋จ")
69
+
70
+ # ์Šคํƒ€์ผ ์ด๋ฏธ์ง€ ์ถ”๊ฐ€ (์žˆ๋Š” ๊ฒฝ์šฐ)
71
+ if style_image_path:
72
+ style_img = Image.open(style_image_path)
73
+ contents.append(style_img)
74
+ logger.debug("์Šคํƒ€์ผ ์ด๋ฏธ์ง€ ์ถ”๊ฐ€๋จ")
75
+
76
+ logger.debug(f"์ปจํ…์ธ  ๊ฐ์ฒด ์ƒ์„ฑ ์™„๋ฃŒ: {len(contents)} ์•„์ดํ…œ")
77
+
78
+ # ์ƒ์„ฑ ์„ค์ •
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
  generate_content_config = types.GenerateContentConfig(
80
  temperature=1,
81
  top_p=0.95,
82
  top_k=40,
83
  max_output_tokens=8192,
84
+ response_modalities=["text", "image"], # ๋ฌธ์„œ์— ๋งž๊ฒŒ ๋Œ€์†Œ๋ฌธ์ž ์ˆ˜์ •
 
 
 
 
85
  )
86
  logger.debug(f"์ƒ์„ฑ ์„ค์ •: {generate_content_config}")
87
 
 
89
  temp_path = tmp.name
90
  logger.debug(f"์ž„์‹œ ํŒŒ์ผ ์ƒ์„ฑ๋จ: {temp_path}")
91
 
92
+ # ์ŠคํŠธ๋ฆฌ๋ฐ ๋Œ€์‹  ๋‹จ์ผ ์š”์ฒญ์œผ๋กœ ๋ณ€๊ฒฝ (์ด๋ฏธ์ง€ ์ƒ์„ฑ์— ๋” ์ ํ•ฉ)
93
+ response = client.models.generate_content(
94
  model=model,
95
  contents=contents,
96
  config=generate_content_config,
97
  )
98
+
99
+ logger.debug("์‘๋‹ต ์ฒ˜๋ฆฌ ์‹œ์ž‘...")
100
+
101
+ # ์‘๋‹ต์—์„œ ์ด๋ฏธ์ง€ ์ถ”์ถœ
102
+ image_saved = False
103
+ for part in response.candidates[0].content.parts:
104
+ if hasattr(part, 'text') and part.text:
105
+ logger.info(f"์ˆ˜์‹ ๋œ ํ…์ŠคํŠธ: {part.text}")
106
+ print(part.text)
107
+ elif hasattr(part, 'inline_data') and part.inline_data:
108
+ save_binary_file(temp_path, part.inline_data.data)
109
+ logger.info(f"MIME ํƒ€์ž… {part.inline_data.mime_type}์˜ ํŒŒ์ผ์ด ์ €์žฅ๋จ: {temp_path}")
110
+ image_saved = True
111
+
112
+ if not image_saved:
113
+ logger.warning("์ด๋ฏธ์ง€๊ฐ€ ์ƒ์„ฑ๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.")
114
+ return None
115
+
116
+ logger.debug("์ด๋ฏธ์ง€ ์ƒ์„ฑ ์™„๋ฃŒ.")
117
  return temp_path
118
 
119
  except Exception as e:
 
146
  style_pil.save(style_path)
147
  logger.debug(f"์Šคํƒ€์ผ ์ด๋ฏธ์ง€ ์ €์žฅ ์™„๋ฃŒ: {style_path}")
148
 
149
+ # ํ”„๋กฌํ”„ํŠธ ๋ณด๊ฐ• (์˜์–ด์™€ ํ•œ๊ตญ์–ด ๋‘ ์–ธ์–ด๋กœ ์ œ๊ณต)
150
+ if prompt and not prompt.strip():
151
+ if background_path and style_path:
152
+ prompt = "์›๋ณธ ์ด๋ฏธ์ง€์˜ ์ธ๋ฌผ์„ ์œ ์ง€ํ•˜๋ฉด์„œ ๋ฐฐ๊ฒฝ์„ ๋‘ ๋ฒˆ์งธ ์ด๋ฏธ์ง€๋กœ ๊ต์ฒดํ•˜๊ณ  ์„ธ ๋ฒˆ์งธ ์ด๋ฏธ์ง€์˜ ์Šคํƒ€์ผ์„ ์ ์šฉํ•ด ์ฃผ์„ธ์š”. Please replace the background with the second image while keeping the person, and apply the style of the third image."
153
+ elif background_path:
154
+ prompt = "์›๋ณธ ์ด๋ฏธ์ง€์˜ ์ธ๋ฌผ์„ ์œ ์ง€ํ•˜๋ฉด์„œ ๋ฐฐ๊ฒฝ์„ ๋‘ ๋ฒˆ์งธ ์ด๋ฏธ์ง€๋กœ ๊ต์ฒดํ•ด ์ฃผ์„ธ์š”. Please replace the background with the second image while keeping the person."
155
+ elif style_path:
156
+ prompt = "์›๋ณธ ์ด๋ฏธ์ง€์— ๋‘ ๋ฒˆ์งธ ์ด๋ฏธ์ง€์˜ ์Šคํƒ€์ผ์„ ์ ์šฉํ•ด ์ฃผ์„ธ์š”. Please apply the style of the second image to the original image."
157
+
158
  model = "gemini-2.0-flash-exp-image-generation"
159
 
160
+ gemma_edited_image_path = generate(
161
+ text=prompt,
162
+ original_image_path=original_path,
163
+ background_image_path=background_path,
164
+ style_image_path=style_path,
165
+ model=model
166
+ )
167
 
168
  if gemma_edited_image_path:
169
  logger.debug(f"์ด๋ฏธ์ง€ ์ƒ์„ฑ ์™„๋ฃŒ. ๊ฒฝ๋กœ: {gemma_edited_image_path}")
170
  result_img = Image.open(gemma_edited_image_path)
171
  if result_img.mode == "RGBA":
172
  result_img = result_img.convert("RGB")
173
+
174
+ # ์ž„์‹œ ํŒŒ์ผ ์ •๋ฆฌ
175
+ try:
176
+ os.unlink(original_path)
177
+ if background_path:
178
+ os.unlink(background_path)
179
+ if style_path:
180
+ os.unlink(style_path)
181
+ except Exception as e:
182
+ logger.warning(f"์ž„์‹œ ํŒŒ์ผ ์‚ญ์ œ ์ค‘ ์˜ค๋ฅ˜: {str(e)}")
183
+
184
  return [result_img]
185
  else:
186
  logger.error("generate ํ•จ์ˆ˜์—์„œ None ๋ฐ˜ํ™˜๋จ.")
 
210
 
211
  with gr.Row():
212
  with gr.Column():
213
+ original_input = gr.Image(type="pil", label="์›๋ณธ ์ด๋ฏธ์ง€", image_mode="RGB")
214
+ background_input = gr.Image(type="pil", label="๋ฐฐ๊ฒฝ ์ด๋ฏธ์ง€", image_mode="RGB")
215
+ style_input = gr.Image(type="pil", label="์Šคํƒ€์ผ ์ด๋ฏธ์ง€", image_mode="RGB")
216
  prompt_input = gr.Textbox(
217
  lines=2,
218
  placeholder="ํŽธ์ง‘ํ•  ๋‚ด์šฉ์„ ์ž…๋ ฅํ•˜์„ธ์š”...",
 
221
  submit_btn = gr.Button("์ด๋ฏธ์ง€ ํŽธ์ง‘ ์‹คํ–‰")
222
  with gr.Column():
223
  output_gallery = gr.Gallery(label="ํŽธ์ง‘ ๊ฒฐ๊ณผ")
224
+ output_text = gr.Textbox(label="API ์‘๋‹ต ํ…์ŠคํŠธ", visible=True)
225
 
226
  submit_btn.click(
227
  fn=process_image_and_prompt,
 
229
  outputs=output_gallery,
230
  )
231
 
232
+ gr.HTML("""
233
+ <div style="margin-top: 20px; padding: 10px; background-color: #f8f9fa; border-radius: 8px;">
234
+ <h3>์‚ฌ์šฉ ํŒ:</h3>
235
+ <ul>
236
+ <li><strong>ํ”„๋กฌํ”„ํŠธ ์ž‘์„ฑ ์˜ˆ์‹œ:</strong> "์›๋ณธ ์ธ๋ฌผ์„ ์œ ์ง€ํ•˜๋ฉด์„œ ๋ฐฐ๊ฒฝ์„ ๋‘ ๋ฒˆ์งธ ์ด๋ฏธ์ง€๋กœ ๊ต์ฒดํ•ด ์ฃผ์„ธ์š”."</li>
237
+ <li><strong>๋ฐฐ๊ฒฝ ๊ต์ฒด ๋ช…ํ™•ํ™”:</strong> "๋ฐฐ๊ฒฝ๋งŒ ์™„์ „ํžˆ ๊ต์ฒดํ•˜๊ณ  ์›๋ณธ ์ธ๋ฌผ์€ ๊ทธ๋Œ€๋กœ ์œ ์ง€ํ•ด ์ฃผ์„ธ์š”."</li>
238
+ <li><strong>์Šคํƒ€์ผ ์ ์šฉ:</strong> "์„ธ ๋ฒˆ์งธ ์ด๋ฏธ์ง€์˜ ์˜ˆ์ˆ  ์Šคํƒ€์ผ์„ ์ „์ฒด ์ด๋ฏธ์ง€์— ์ ์šฉํ•ด ์ฃผ์„ธ์š”."</li>
239
+ <li><strong>์˜์–ด ํ”„๋กฌํ”„ํŠธ:</strong> ๋” ๋‚˜์€ ๊ฒฐ๊ณผ๋ฅผ ์œ„ํ•ด ์˜์–ด์™€ ํ•œ๊ตญ์–ด๋ฅผ ํ•จ๊ป˜ ์‚ฌ์šฉํ•ด ๋ณด์„ธ์š”.</li>
240
+ </ul>
241
+ </div>
242
+ """)
243
+
244
+ # --- ์‹คํ–‰ ---
245
+ demo.launch(share=True)