File size: 10,729 Bytes
8d852cf
 
 
 
 
 
 
1e9bbd5
8d852cf
ddcfe75
 
8d852cf
e0e8bcb
 
 
 
 
8d852cf
 
 
 
ddcfe75
8d852cf
e0e8bcb
8d852cf
 
e0e8bcb
8d852cf
ddcfe75
654ca6d
 
1e9bbd5
8d852cf
e0e8bcb
 
 
 
 
 
 
8d852cf
 
e0e8bcb
8d852cf
1e9bbd5
811d552
 
1e9bbd5
654ca6d
 
 
 
 
 
 
 
 
 
 
1e9bbd5
 
 
8d852cf
 
 
 
 
811d552
8d852cf
e0e8bcb
8d852cf
 
 
e0e8bcb
8d852cf
811d552
1e9bbd5
8d852cf
 
 
 
1e9bbd5
 
 
811d552
1e9bbd5
811d552
 
1e9bbd5
 
811d552
1e9bbd5
 
 
 
 
 
 
 
811d552
1e9bbd5
 
811d552
8d852cf
 
e0e8bcb
811d552
ddcfe75
8d852cf
654ca6d
811d552
993c58c
811d552
 
654ca6d
 
 
 
811d552
 
 
654ca6d
 
 
 
811d552
 
 
 
 
 
 
 
 
 
 
654ca6d
 
 
 
 
 
 
 
811d552
 
654ca6d
811d552
 
654ca6d
811d552
1e9bbd5
993c58c
811d552
 
 
 
8d852cf
 
1e9bbd5
 
 
811d552
 
654ca6d
 
1e9bbd5
 
 
811d552
8d852cf
654ca6d
811d552
ddcfe75
8d852cf
811d552
 
ddcfe75
8d852cf
e0e8bcb
8d852cf
 
 
 
e0e8bcb
 
 
 
654ca6d
 
e0e8bcb
8d852cf
 
 
654ca6d
8d852cf
 
 
654ca6d
 
 
8d852cf
bfd9212
654ca6d
811d552
8d852cf
811d552
8d852cf
811d552
 
8d852cf
 
811d552
654ca6d
811d552
8d852cf
e0e8bcb
1e9bbd5
 
 
 
654ca6d
 
 
 
1e9bbd5
654ca6d
1e9bbd5
 
 
 
 
811d552
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
import json
import os
import time
import tempfile
from PIL import Image
import gradio as gr
import logging
from io import BytesIO

from google import genai
from google.genai import types

# .env νŒŒμΌμ— μ €μž₯된 ν™˜κ²½λ³€μˆ˜ λ‘œλ“œ (python-dotenv μ„€μΉ˜ ν•„μš”: pip install python-dotenv)
from dotenv import load_dotenv
load_dotenv()

# λ‘œκΉ… μ„€μ • (둜그 레벨: DEBUG)
logging.basicConfig(level=logging.DEBUG,
                    format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)


def save_binary_file(file_name, data):
    logger.debug(f"νŒŒμΌμ— 이진 데이터 μ €μž₯ 쀑: {file_name}")
    with open(file_name, "wb") as f:
        f.write(data)
    logger.debug(f"파일 μ €μž₯ μ™„λ£Œ: {file_name}")


def merge_images(person_img_path, product_img_path, background_img_path, prompt, model="gemini-2.0-flash-exp-image-generation"):
    logger.debug(f"merge_images ν•¨μˆ˜ μ‹œμž‘ - ν”„λ‘¬ν”„νŠΈ: '{prompt}'")
    
    try:
        # API ν‚€λŠ” ν™˜κ²½λ³€μˆ˜μ—μ„œ 뢈러옴
        effective_api_key = os.environ.get("GEMINI_API_KEY")
        if effective_api_key:
            logger.debug("ν™˜κ²½λ³€μˆ˜μ—μ„œ API ν‚€ 뢈러옴")
        else:
            logger.error("API ν‚€κ°€ ν™˜κ²½λ³€μˆ˜μ— μ„€μ •λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€.")
            raise ValueError("API ν‚€κ°€ ν•„μš”ν•©λ‹ˆλ‹€.")

        client = genai.Client(api_key=effective_api_key)
        logger.debug("Gemini ν΄λΌμ΄μ–ΈνŠΈ μ΄ˆκΈ°ν™” μ™„λ£Œ.")

        # PIL 이미지 객체둜 λ³€ν™˜
        person_img = Image.open(person_img_path)
        product_img = Image.open(product_img_path)
        
        # 컨텐츠 리슀트 생성
        contents = [person_img, product_img]
        
        # λ°°κ²½ 이미지가 있으면 μΆ”κ°€
        if background_img_path:
            background_img = Image.open(background_img_path)
            contents.append(background_img)
            logger.debug("λ°°κ²½ 이미지 좔가됨")
        
        # λ§ˆμ§€λ§‰μ— ν”„λ‘¬ν”„νŠΈ μΆ”κ°€
        contents.append(prompt)
        logger.debug(f"컨텐츠 객체 생성 μ™„λ£Œ: {len(contents)} μ•„μ΄ν…œ")

        # 생성 μ„€μ •
        generate_content_config = types.GenerateContentConfig(
            temperature=1,
            top_p=0.95,
            top_k=40,
            max_output_tokens=8192,
            response_modalities=["text", "image"],
        )
        logger.debug(f"생성 μ„€μ •: {generate_content_config}")

        with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmp:
            temp_path = tmp.name
            logger.debug(f"μž„μ‹œ 파일 생성됨: {temp_path}")

            # 단일 μš”μ²­μœΌλ‘œ 이미지 생성
            response = client.models.generate_content(
                model=model,
                contents=contents,
                config=generate_content_config,
            )
            
            logger.debug("응닡 처리 μ‹œμž‘...")
            
            # μ‘λ‹΅μ—μ„œ 이미지와 ν…μŠ€νŠΈ μΆ”μΆœ
            image_saved = False
            response_text = ""
            
            for part in response.candidates[0].content.parts:
                if hasattr(part, 'text') and part.text:
                    response_text += part.text
                    logger.info(f"μˆ˜μ‹ λœ ν…μŠ€νŠΈ: {part.text}")
                elif hasattr(part, 'inline_data') and part.inline_data:
                    save_binary_file(temp_path, part.inline_data.data)
                    logger.info(f"MIME νƒ€μž… {part.inline_data.mime_type}의 파일이 μ €μž₯됨: {temp_path}")
                    image_saved = True

            if not image_saved:
                logger.warning("이미지가 μƒμ„±λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€.")
                return None, response_text

        logger.debug("이미지 생성 μ™„λ£Œ.")
        return temp_path, response_text

    except Exception as e:
        logger.exception("이미지 생성 쀑 였λ₯˜ λ°œμƒ:")
        return None, str(e)  # 였λ₯˜ λ°œμƒ μ‹œ Noneκ³Ό 였λ₯˜ λ©”μ‹œμ§€ λ°˜ν™˜


def process_images_and_prompt(person_pil, product_pil, background_pil, prompt):
    logger.debug(f"process_images_and_prompt ν•¨μˆ˜ μ‹œμž‘ - ν”„λ‘¬ν”„νŠΈ: '{prompt}'")
    try:
        # κΈ°λ³Έ ν”„λ‘¬ν”„νŠΈ μ„€μ • (λΉ„μ–΄μžˆλŠ” 경우)
        if not prompt or not prompt.strip():
            if background_pil:
                prompt = "이 배경에 이 μ‚¬λžŒμ΄ 이 μƒν’ˆμ„ μ‚¬μš©ν•˜λŠ” λͺ¨μŠ΅μ„ μžμ—°μŠ€λŸ½κ²Œ λ³΄μ—¬μ£Όμ„Έμš”. μƒν’ˆμ„ 잘 보이게 ν•΄μ£Όμ„Έμš”. Create a natural composite image showing this person using this product in this background setting. Make sure the product is clearly visible."
            else:
                prompt = "이 μ‚¬λžŒμ΄ 이 μƒν’ˆμ„ μ‚¬μš©ν•˜λŠ” λͺ¨μŠ΅μ„ μžμ—°μŠ€λŸ½κ²Œ λ³΄μ—¬μ£Όμ„Έμš”. μƒν’ˆμ„ 잘 보이게 ν•΄μ£Όμ„Έμš”. Create a natural composite image showing this person using this product. Make sure the product is clearly visible."
        
        # ν”„λ‘¬ν”„νŠΈμ— μ˜μ–΄κ°€ μ—†μœΌλ©΄ μ˜μ–΄ ν”„λ‘¬ν”„νŠΈ μΆ”κ°€ (더 λ‚˜μ€ κ²°κ³Όλ₯Ό μœ„ν•΄)
        if not any(ord(c) < 128 for c in prompt):
            if background_pil:
                prompt += " Create a realistic composite image of this person with this product in this background."
            else:
                prompt += " Create a realistic composite image of this person with this product."
        
        # 이미지 μ €μž₯
        with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmp_person:
            person_path = tmp_person.name
            person_pil.save(person_path)
            logger.debug(f"μ‚¬λžŒ 이미지 μ €μž₯ μ™„λ£Œ: {person_path}")
            
        with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmp_product:
            product_path = tmp_product.name
            product_pil.save(product_path)
            logger.debug(f"μƒν’ˆ 이미지 μ €μž₯ μ™„λ£Œ: {product_path}")
        
        # λ°°κ²½ 이미지 μ €μž₯ (μžˆλŠ” 경우)
        background_path = None
        if background_pil is not None:
            with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmp_bg:
                background_path = tmp_bg.name
                background_pil.save(background_path)
                logger.debug(f"λ°°κ²½ 이미지 μ €μž₯ μ™„λ£Œ: {background_path}")

        # 이미지 ν•©μ„± μ‹€ν–‰
        result_path, response_text = merge_images(
            person_img_path=person_path,
            product_img_path=product_path,
            background_img_path=background_path,
            prompt=prompt
        )

        # 이미지 λ°˜ν™˜ 및 μž„μ‹œ 파일 정리
        if result_path:
            logger.debug(f"이미지 생성 μ™„λ£Œ. 경둜: {result_path}")
            result_img = Image.open(result_path)
            if result_img.mode == "RGBA":
                result_img = result_img.convert("RGB")
            
            # μž„μ‹œ 파일 정리
            try:
                os.unlink(person_path)
                os.unlink(product_path)
                if background_path:
                    os.unlink(background_path)
            except Exception as e:
                logger.warning(f"μž„μ‹œ 파일 μ‚­μ œ 쀑 였λ₯˜: {str(e)}")
            
            return [result_img], response_text
        else:
            logger.error("merge_images ν•¨μˆ˜μ—μ„œ None λ°˜ν™˜λ¨.")
            return [], response_text  # 였λ₯˜ μ‹œ 빈 리슀트 λ°˜ν™˜

    except Exception as e:
        logger.exception("process_images_and_prompt ν•¨μˆ˜μ—μ„œ 였λ₯˜ λ°œμƒ:")
        return [], str(e)  # 였λ₯˜ μ‹œ 빈 λ¦¬μŠ€νŠΈμ™€ 였λ₯˜ λ©”μ‹œμ§€ λ°˜ν™˜


# --- Gradio μΈν„°νŽ˜μ΄μŠ€ ꡬ성 ---
with gr.Blocks() as demo:
    gr.HTML(
        """
        <div style='display: flex; align-items: center; justify-content: center; gap: 20px'>
            <div style="background-color: var(--block-background-fill); border-radius: 8px">
                <img src="https://www.gstatic.com/lamda/images/gemini_favicon_f069958c85030456e93de685481c559f160ea06b.png" style="width: 100px; height: 100px;">
            </div>
            <div>
                <h1>Geminiλ₯Ό μ΄μš©ν•œ 이미지 ν•©μ„±</h1>
                <p>μ‚¬λžŒ, μƒν’ˆ, λ°°κ²½ 이미지λ₯Ό ν•©μ„±ν•˜λŠ” μ• ν”Œλ¦¬μΌ€μ΄μ…˜μž…λ‹ˆλ‹€.</p>
            </div>
        </div>
        """
    )
    gr.Markdown("μ‚¬λžŒ 이미지, μƒν’ˆ 이미지, λ°°κ²½ 이미지λ₯Ό μ—…λ‘œλ“œν•˜κ³ , μ–΄λ–»κ²Œ ν•©μ„±ν• μ§€ μ„€λͺ…ν•΄μ£Όμ„Έμš”.")

    with gr.Row():
        with gr.Column():
            person_input = gr.Image(type="pil", label="μ‚¬λžŒ 이미지 (ν•„μˆ˜)", image_mode="RGB")
            product_input = gr.Image(type="pil", label="μƒν’ˆ 이미지 (ν•„μˆ˜)", image_mode="RGB")
            background_input = gr.Image(type="pil", label="λ°°κ²½ 이미지 (선택 사항)", image_mode="RGB")
            prompt_input = gr.Textbox(
                lines=2,
                placeholder="ν•©μ„± 방법을 μ„€λͺ…ν•΄μ£Όμ„Έμš”. (예: '이 λ°°κ²½μ—μ„œ 이 μ‚¬λžŒμ΄ μƒν’ˆμ„ μ‚¬μš©ν•˜λŠ” λͺ¨μŠ΅' λ˜λŠ” '이 μ‚¬λžŒμ΄ 이 μƒν’ˆμ„ λ“€κ³  이 배경에 μ„œ μžˆλŠ” λͺ¨μŠ΅')",
                label="ν•©μ„± 방법 μ„€λͺ…"
            )
            submit_btn = gr.Button("이미지 ν•©μ„± μ‹€ν–‰")
        with gr.Column():
            output_gallery = gr.Gallery(label="ν•©μ„± κ²°κ³Ό")
            output_text = gr.Textbox(label="AI 응닡 ν…μŠ€νŠΈ", visible=True)

    submit_btn.click(
        fn=process_images_and_prompt,
        inputs=[person_input, product_input, background_input, prompt_input],
        outputs=[output_gallery, output_text],
    )

    gr.HTML("""
    <div style="margin-top: 20px; padding: 10px; background-color: #f8f9fa; border-radius: 8px;">
        <h3>μ‚¬μš© 팁:</h3>
        <ul>
            <li><strong>λ°°κ²½ ν™œμš©:</strong> "이 λ°°κ²½μ—μ„œ 이 μ‚¬λžŒμ΄ 이 μƒν’ˆμ„ μ‚¬μš©ν•˜λŠ” μžμ—°μŠ€λŸ¬μš΄ λͺ¨μŠ΅μ„ λ§Œλ“€μ–΄μ£Όμ„Έμš”."</li>
            <li><strong>νŠΉμ • μœ„μΉ˜ μ§€μ •:</strong> "이 μ‚¬λžŒμ΄ 이 μƒν’ˆμ„ 손에 λ“€κ³  이 λ°°κ²½ μ•žμ— μ„œ μžˆλŠ” λͺ¨μŠ΅μ„ λ³΄μ—¬μ£Όμ„Έμš”."</li>
            <li><strong>μƒν’ˆ κ°•μ‘°:</strong> "이 μ‚¬λžŒμ΄ 이 λ°°κ²½μ—μ„œ 이 μƒν’ˆμ„ μ‚¬μš©ν•˜λŠ” λͺ¨μŠ΅μ„ λ³΄μ—¬μ£Όλ˜, μƒν’ˆμ΄ 잘 보이도둝 ν•΄μ£Όμ„Έμš”."</li>
            <li><strong>μž₯λ©΄ μ„€μ •:</strong> "이 μ‚¬λžŒμ΄ 이 μƒν’ˆμœΌλ‘œ μš”λ¦¬ν•˜λŠ” λͺ¨μŠ΅μ„ 이 μ£Όλ°© λ°°κ²½μ—μ„œ λ³΄μ—¬μ£Όμ„Έμš”."</li>
            <li><strong>μ˜μ–΄ ν”„λ‘¬ν”„νŠΈ:</strong> 더 λ‚˜μ€ κ²°κ³Όλ₯Ό μœ„ν•΄ μ˜μ–΄μ™€ ν•œκ΅­μ–΄λ₯Ό ν•¨κ»˜ μ‚¬μš©ν•΄ λ³΄μ„Έμš”.</li>
            <li><strong>λ°°κ²½ 선택사항:</strong> λ°°κ²½ μ΄λ―Έμ§€λŠ” μ„ νƒμ‚¬ν•­μž…λ‹ˆλ‹€. λ°°κ²½ 없이 μ‚¬λžŒκ³Ό μƒν’ˆλ§Œ ν•©μ„±ν•  μˆ˜λ„ μžˆμŠ΅λ‹ˆλ‹€.</li>
        </ul>
    </div>
    """)

# --- μ‹€ν–‰ ---
if __name__ == "__main__":
    demo.launch(share=True)