File size: 9,201 Bytes
d097d3c
c9f16b7
 
4ca74ac
 
 
c9f16b7
 
4ca74ac
86eb4e0
4ca74ac
 
a0a6c5e
4ca74ac
 
 
 
 
 
 
c9f16b7
4ca74ac
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a0a6c5e
4ca74ac
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c9f16b7
4ca74ac
c9f16b7
4ca74ac
c9f16b7
 
4ca74ac
 
 
 
c9f16b7
4ca74ac
 
 
 
c9f16b7
 
 
4ca74ac
c9f16b7
 
a0a6c5e
4ca74ac
 
c9f16b7
a0a6c5e
 
 
 
 
5cb1799
a0a6c5e
 
 
4ca74ac
a0a6c5e
 
4ca74ac
a0a6c5e
 
c9f16b7
 
 
 
4ca74ac
af5aa6e
c9f16b7
a0a6c5e
4ca74ac
a0a6c5e
 
 
c9f16b7
a0a6c5e
c9f16b7
a0a6c5e
 
 
 
 
 
d097d3c
c9f16b7
a0a6c5e
c9f16b7
4ca74ac
c9f16b7
 
 
 
 
a0a6c5e
c9f16b7
4ca74ac
c9f16b7
4ca74ac
c9f16b7
 
 
35bdae0
c9f16b7
 
 
 
 
 
 
 
 
c4d9900
c9f16b7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d097d3c
c9f16b7
d9997b1
c9f16b7
 
 
 
4ca74ac
 
 
 
 
 
c9f16b7
 
 
 
 
a0a6c5e
 
c9f16b7
 
a0a6c5e
c9f16b7
a0a6c5e
 
 
 
 
 
c9f16b7
a0a6c5e
 
 
 
e507f42
 
c9f16b7
a0a6c5e
 
c9f16b7
 
578cfa6
c9f16b7
a0a6c5e
c9f16b7
4ca74ac
a0a6c5e
e06bc2a
c9f16b7
 
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
import os
import tempfile
import gradio as gr
import pandas as pd
from urllib.parse import urlparse, parse_qs
from PIL import Image
from io import BytesIO
import requests
from google import genai

# 請確保你的 requirements.txt 包含 pandas
# pip install pandas

# --- Google Sheets 相關函式 (已更新) ---
def read_google_sheet(sheet_url: str):
    """
    從 Google Sheet 的 URL 讀取資料。
    """
    if not sheet_url:
        raise gr.Error("請提供 Google Sheet URL。")
    try:
        def build_csv_url(url: str) -> str:
            parsed = urlparse(url)
            doc_id = parsed.path.strip("/").split("/")[2] if len(parsed.path.strip("/").split("/")) >= 3 and parsed.path.strip("/").split("/")[1] == "d" else None
            gid = parse_qs(parsed.query).get("gid", [None])[0] or parse_qs(parsed.fragment).get("gid", [None])[0] or "0"
            if doc_id:
                return f"https://docs.google.com/spreadsheets/d/{doc_id}/export?format=csv&gid={gid}"
            if "/export" in parsed.path and "format=csv" in parsed.query:
                return url
            return url.replace("/edit#gid=0", "/export?format=csv&gid=0")

        csv_url = build_csv_url(sheet_url)
        df = pd.read_csv(csv_url, engine='python', on_bad_lines='warn', encoding='utf-8')
        return df
    except Exception as e:
        raise gr.Error(f"讀取 Google Sheet 時發生錯誤: {e}")

def process_sheet_data(sheet_url):
    """處理試算表資料,為 Gradio DataFrame 準備。"""
    try:
        df = read_google_sheet(sheet_url)
        if df.shape[1] < 4:  # 檢查是否至少有 4 列 (索引, 白背圖URL, 參考圖URL, 提示詞)
            error_msg = f"錯誤:Google Sheet 至少需要 4 列 (索引, 白背圖URL, 參考圖URL, 提示詞)。目前只有 {df.shape[1]} 列。"
            raise gr.Error(error_msg)
        
        # 假設列順序為: White_Back_Image_URL, Ref_Image_URL, Prompt
        headers = ["Index", "白背圖URL", "參考圖URL", "提示詞"]
        data_list = []
        for i, row in df.iterrows():
            if pd.notna(row.iloc[0]):  # 檢查第一列 (白背圖URL) 是否有資料
                data_list.append([i + 2, row.iloc[0], row.iloc[1], row.iloc[2]])
        
        log_message = f"成功讀取 {len(data_list)} 筆數據。"
        return data_list, log_message
    except Exception as e:
        raise gr.Error(f"處理試算表資料時發生錯誤: {e}")

def get_row_data(sheet_url, row_number):
    """從 Google Sheet 讀取指定列的資料。"""
    try:
        df = read_google_sheet(sheet_url)
        row_index = int(row_number) - 2 # 由於DataFrame索引從0開始,而Gradio顯示的行數從2開始
        if row_index < 0 or row_index >= df.shape[0]:
            raise gr.Error(f"指定的行數 {row_number} 不存在。")
        
        row_data = df.iloc[row_index]
        white_back_image_url = row_data.iloc[0] if pd.notna(row_data.iloc[0]) else ""
        ref_image_url = row_data.iloc[1] if pd.notna(row_data.iloc[1]) else ""
        prompt_text = row_data.iloc[2] if pd.notna(row_data.iloc[2]) else ""
        
        return white_back_image_url, ref_image_url, prompt_text
    except Exception as e:
        raise gr.Error(f"讀取指定行時發生錯誤: {e}")

# --- 下載圖片函式 ---
def load_image_from_url(url: str):
    """從 URL 下載圖片並以 PIL Image 格式回傳。"""
    if not url or not isinstance(url, str):
        return None
    try:
        headers = {
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
        }
        response = requests.get(url, timeout=20, headers=headers)
        response.raise_for_status()
        image = Image.open(BytesIO(response.content)).convert("RGB")
        return image
    except requests.exceptions.HTTPError as e:
        gr.Warning(f"下載圖片失敗:HTTP 錯誤 {e.response.status_code}")
        return None
    except Exception as e:
        gr.Warning(f"下載圖片時發生意外錯誤:{e}")
        return None

# --- Gemini 核心函式 ---
def generate_image(text, images, api_key, model="gemini-2.5-flash-image-preview"):
    """使用 Gemini 模型生成圖片。"""
    if not api_key or api_key.strip() == "":
        raise gr.Error("請輸入有效的 Gemini API 金鑰。", duration=10)
    try:
        client = genai.Client(api_key=api_key.strip())
        contents = images + [text]
        response = client.models.generate_content(model=model, contents=contents)
        text_response = ""
        image_path = None
        for part in response.candidates[0].content.parts:
            if part.text is not None:
                text_response += part.text + "\n"
            elif part.inline_data is not None:
                with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmp:
                    temp_path = tmp.name
                    generated_image = Image.open(BytesIO(part.inline_data.data))
                    generated_image.save(temp_path)
                    image_path = temp_path
        return image_path, text_response
    except Exception as e:
        raise gr.Error(f"Gemini API 呼叫失敗: {e}", duration=10)

# --- Gradio 互動函式 ---
def generate_image_from_row(sheet_url, row_number, gemini_api_key):
    """根據指定的行數,生成圖片。"""
    if not sheet_url:
        raise gr.Error("請先輸入 Google Sheet URL。", duration=5)
    if not row_number or row_number <= 1:
        raise gr.Error("請輸入有效的行數 (大於 1)。", duration=5)
        
    try:
        white_back_url, ref_image_url, prompt_text = get_row_data(sheet_url, row_number)
        
        log_message = f"開始處理第 {row_number} 行...\n"
        log_message += f"白背圖URL: {white_back_url}\n"
        log_message += f"參考圖URL: {ref_image_url}\n"
        log_message += f"提示詞: {prompt_text}\n"

        images_for_gemini = []
        if white_back_url:
            wb_img = load_image_from_url(white_back_url)
            if wb_img:
                images_for_gemini.append(wb_img)
        if ref_image_url:
            ref_img = load_image_from_url(ref_image_url)
            if ref_img:
                images_for_gemini.append(ref_img)

        if not images_for_gemini:
            return None, "警告:無效的圖片 URL,無法生成圖片。"
        
        log_message += "圖片已下載,開始呼叫 Gemini 模型...\n"
        
        image_path, text_response = generate_image(text=prompt_text, images=images_for_gemini, api_key=gemini_api_key)

        if image_path:
            log_message += "圖片生成成功!"
            return image_path, log_message
        else:
            log_message += "圖片生成失敗。\n"
            log_message += f"Gemini 文字回應: {text_response}"
            return None, log_message
    except ValueError:
        raise gr.Error("行數必須為數字。", duration=5)
    except Exception as e:
        return None, f"生成圖片時發生錯誤: {e}"

# --- Gradio 介面設定 ---
with gr.Blocks() as demo:
    gr.Markdown("## Google Sheets 圖片生成器")
    
    with gr.Row(elem_classes="main-content"):
        with gr.Column(elem_classes="input-column"):
            gemini_api_key = gr.Textbox(
                lines=1,
                placeholder="請在此輸入你的 Gemini API 金鑰",
                label="Gemini API 金鑰",
                elem_classes="api-key-input"
            )
            sheet_url_input = gr.Textbox(
                label="Google Sheet URL", 
                value="https://docs.google.com/spreadsheets/d/1G3olHxydDIbnyXdh5nnw5TG0akZFeMeYm-25JmCGDLg/edit?gid=0#gid=0"
            )
            process_button = gr.Button("處理試算表", elem_classes="generate-btn")
            
            with gr.Row():
                row_index_input = gr.Number(label="要生成的行數", precision=0, value=2)
                generate_selected_button = gr.Button("生成所選行的圖片", elem_classes="generate-btn")
        
        with gr.Column(elem_classes="output-column"):
            output_dataframe = gr.DataFrame(
                headers=["Index", "白背圖URL", "參考圖URL", "提示詞"],
                col_count=(4, "fixed"),
                interactive=False,
                label="已處理的試算表數據"
            )
            generated_image_output = gr.Image(label="生成的圖片", elem_classes="output-gallery")
            operation_log_output = gr.Textbox(
                label="操作日誌", 
                lines=10,
                placeholder="文字回應和日誌會顯示在這裡。"
            )

    # 按鈕的事件綁定
    process_button.click(
        fn=process_sheet_data,
        inputs=[sheet_url_input],
        outputs=[output_dataframe, operation_log_output]
    )

    generate_selected_button.click(
        fn=generate_image_from_row,
        inputs=[sheet_url_input, row_index_input, gemini_api_key],
        outputs=[generated_image_output, operation_log_output]
    )
    
    demo.queue().launch(mcp_server=True, share=True)