davidlee831117 commited on
Commit
27735e2
·
verified ·
1 Parent(s): b3722ab

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +237 -0
app.py ADDED
@@ -0,0 +1,237 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import tempfile
3
+ import gradio as gr
4
+ import pandas as pd
5
+ from urllib.parse import urlparse, parse_qs
6
+ from PIL import Image
7
+ from io import BytesIO
8
+ import requests
9
+ from google import genai
10
+
11
+ # --- Google Sheets 相關函式 ---
12
+ def read_google_sheet(sheet_url: str):
13
+ """
14
+ 從 Google Sheet 的 URL 讀取資料。
15
+ """
16
+ if not sheet_url:
17
+ raise gr.Error("請提供 Google Sheet URL。")
18
+ try:
19
+ def build_csv_url(url: str) -> str:
20
+ parsed = urlparse(url)
21
+ doc_id = parsed.path.strip("/").split("/")[2] if len(parsed.path.strip("/").split("/")) >= 3 and parsed.path.strip("/").split("/")[1] == "d" else None
22
+ gid = parse_qs(parsed.query).get("gid", [None])[0] or parse_qs(parsed.fragment).get("gid", [None])[0] or "0"
23
+ if doc_id:
24
+ return f"https://docs.google.com/spreadsheets/d/{doc_id}/export?format=csv&gid={gid}"
25
+ if "/export" in parsed.path and "format=csv" in parsed.query:
26
+ return url
27
+ return url.replace("/edit#gid=0", "/export?format=csv&gid=0")
28
+
29
+ csv_url = build_csv_url(sheet_url)
30
+ # 增加 read_csv 的 robustness,防止某些 Google Sheet 導致的 ParserError
31
+ df = pd.read_csv(csv_url, engine='python', on_bad_lines='warn', encoding='utf-8')
32
+ return df
33
+ except Exception as e:
34
+ raise gr.Error(f"讀取 Google Sheet 時發生錯誤: {e}")
35
+
36
+ def process_sheet_data(sheet_url):
37
+ """處理試算表資料,為 Gradio DataFrame 準備。"""
38
+ try:
39
+ df = read_google_sheet(sheet_url)
40
+ # 檢查是否至少有 3 列 (白背圖URL, 參考圖URL, 提示詞),因為索引會在程式中生成
41
+ if df.shape[1] < 3:
42
+ error_msg = f"錯誤:Google Sheet 至少需要 3 列 (白背圖URL, 參考圖URL, 提示詞)。目前只有 {df.shape[1]} 列。"
43
+ raise gr.Error(error_msg)
44
+
45
+ data_list = []
46
+ for i, row in df.iterrows():
47
+ # 確保有足夠的列且至少第一列(白背圖URL)不為空才加入
48
+ if len(row) >= 3 and pd.notna(row.iloc[0]):
49
+ data_list.append([i + 2, row.iloc[0], row.iloc[1], row.iloc[2]])
50
+
51
+ log_message = f"成功讀取 {len(data_list)} 筆數據。"
52
+ return data_list, log_message
53
+ except Exception as e:
54
+ raise gr.Error(f"處理試算表資料時發生錯誤: {e}")
55
+
56
+ def get_row_data(sheet_url, row_number):
57
+ """從 Google Sheet 讀取指定列的資料。"""
58
+ try:
59
+ df = read_google_sheet(sheet_url)
60
+ # Gradio顯示的行數從2開始,DataFrame索引從0開始,所以需要減2
61
+ row_index = int(row_number) - 2
62
+ if row_index < 0 or row_index >= df.shape[0]:
63
+ raise gr.Error(f"指定的行數 {row_number} 不存在。")
64
+
65
+ row_data = df.iloc[row_index]
66
+ # 確保取值時索引不越界,並處理 NaN
67
+ white_back_image_url = row_data.iloc[0] if len(row_data) > 0 and pd.notna(row_data.iloc[0]) else ""
68
+ ref_image_url = row_data.iloc[1] if len(row_data) > 1 and pd.notna(row_data.iloc[1]) else ""
69
+ prompt_text = row_data.iloc[2] if len(row_data) > 2 and pd.notna(row_data.iloc[2]) else ""
70
+
71
+ return white_back_image_url, ref_image_url, prompt_text
72
+ except Exception as e:
73
+ raise gr.Error(f"讀取指定行時發生錯誤: {e}")
74
+
75
+ # --- 下載圖片函式 ---
76
+ def load_image_from_url(url: str):
77
+ """從 URL 下載圖片並以 PIL Image 格式回傳。"""
78
+ if not url or not isinstance(url, str) or not url.strip(): # 增加對空字串的檢查
79
+ return None
80
+ try:
81
+ headers = {
82
+ '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',
83
+ }
84
+ response = requests.get(url, timeout=20, headers=headers)
85
+ response.raise_for_status()
86
+ image = Image.open(BytesIO(response.content)).convert("RGB")
87
+ return image
88
+ except requests.exceptions.HTTPError as e:
89
+ gr.Warning(f"下載圖片失敗:HTTP 錯誤 {e.response.status_code}")
90
+ return None
91
+ except Exception as e:
92
+ gr.Warning(f"下載圖片時發生意外錯誤:{e}")
93
+ return None
94
+
95
+ # --- Gemini 核心函式 ---
96
+ def generate_image(text, images, api_key, model="gemini-2.5-flash-image-preview"):
97
+ """使用 Gemini 模型生成圖片。"""
98
+ if not api_key or api_key.strip() == "":
99
+ raise gr.Error("請輸入有效的 Gemini API 金鑰。", duration=10)
100
+ try:
101
+ client = genai.Client(api_key=api_key.strip())
102
+ contents = images + [text]
103
+ response = client.models.generate_content(model=model, contents=contents)
104
+ text_response = ""
105
+ image_path = None
106
+ for part in response.candidates[0].content.parts:
107
+ if part.text is not None:
108
+ text_response += part.text + "\n"
109
+ elif part.inline_data is not None:
110
+ with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmp:
111
+ temp_path = tmp.name
112
+ generated_image = Image.open(BytesIO(part.inline_data.data))
113
+ generated_image.save(temp_path)
114
+ image_path = temp_path
115
+ return image_path, text_response
116
+ except Exception as e:
117
+ raise gr.Error(f"Gemini API 呼叫失敗: {e}", duration=10)
118
+
119
+ # --- Gradio 互動函式 ---
120
+ def generate_image_from_row(sheet_url, row_number, gemini_api_key):
121
+ """
122
+ 根據指定的行數,生成圖片,並返回白背圖、參考圖和生成的圖片。
123
+ """
124
+ if not sheet_url:
125
+ raise gr.Error("請先輸入 Google Sheet URL。", duration=5)
126
+ if not row_number or row_number <= 1:
127
+ raise gr.Error("請輸入有效的行數 (大於 1)。", duration=5)
128
+
129
+ try:
130
+ white_back_url, ref_image_url, prompt_text = get_row_data(sheet_url, row_number)
131
+
132
+ log_message = f"開始處理第 {row_number} 行...\n"
133
+ log_message += f"白背圖URL: {white_back_url if white_back_url else '無'}\n"
134
+ log_message += f"參考圖URL: {ref_image_url if ref_image_url else '無'}\n"
135
+ log_message += f"提示詞: {prompt_text if prompt_text else '無'}\n"
136
+
137
+ images_for_gemini = []
138
+
139
+ # 下載圖片
140
+ wb_img = None
141
+ if white_back_url:
142
+ wb_img = load_image_from_url(white_back_url)
143
+ if wb_img:
144
+ images_for_gemini.append(wb_img)
145
+ else:
146
+ log_message += f"警告:無法下載白背圖 '{white_back_url}'。\n"
147
+
148
+ ref_img = None
149
+ if ref_image_url:
150
+ ref_img = load_image_from_url(ref_image_url)
151
+ if ref_img:
152
+ images_for_gemini.append(ref_img)
153
+ else:
154
+ log_message += f"警告:無法下載參考圖 '{ref_image_url}'。\n"
155
+
156
+ if not images_for_gemini:
157
+ # 如果沒有任何圖片,但有提示詞,嘗試純文字生成
158
+ if prompt_text:
159
+ log_message += "沒有可用的輸入圖片,嘗試僅使用提示詞生成圖片。\n"
160
+ else:
161
+ return None, None, None, "警告:無效的圖片 URL 且無提示詞,無法生成圖片。", log_message
162
+
163
+ log_message += "圖片已下載,開始呼叫 Gemini 模型...\n"
164
+
165
+ image_path, text_response = generate_image(text=prompt_text, images=images_for_gemini, api_key=gemini_api_key)
166
+
167
+ if image_path:
168
+ log_message += "圖片生成成功!"
169
+ return wb_img, ref_img, image_path, log_message
170
+ else:
171
+ log_message += "圖片生成失敗。\n"
172
+ log_message += f"Gemini 文字回應: {text_response}"
173
+ return wb_img, ref_img, None, log_message
174
+
175
+ except ValueError:
176
+ return None, None, None, "行數必須為數字。", "生成圖片時發生錯誤:行數必須為數字。"
177
+ except Exception as e:
178
+ return None, None, None, f"生成圖片時發生錯誤: {e}", f"生成圖片時發生錯誤: {e}"
179
+
180
+ # --- Gradio 介面設定 ---
181
+ with gr.Blocks() as demo:
182
+ gr.Markdown("## Google Sheets 圖片生成器")
183
+
184
+ with gr.Row(elem_classes="main-content"):
185
+ with gr.Column(elem_classes="input-column"):
186
+ gemini_api_key = gr.Textbox(
187
+ lines=1,
188
+ placeholder="請在此輸入你的 Gemini API 金鑰",
189
+ label="Gemini API 金鑰",
190
+ elem_classes="api-key-input",
191
+ # 從環境變數讀取預設值 (如果存在)
192
+ value=os.environ.get("GEMINI_API_KEY", "")
193
+ )
194
+ sheet_url_input = gr.Textbox(
195
+ label="Google Sheet URL",
196
+ value="https://docs.google.com/spreadsheets/d/1G3olHxydDIbnyXdh5nnw5TG0akZFeMeYm-25JmCGDLg/edit?gid=0#gid=0"
197
+ )
198
+ process_button = gr.Button("處理試算表", elem_classes="generate-btn")
199
+
200
+ with gr.Row():
201
+ row_index_input = gr.Number(label="要生成的行數", precision=0, value=2)
202
+ generate_selected_button = gr.Button("生成所選行的圖片", elem_classes="generate-btn")
203
+
204
+ with gr.Column(elem_classes="output-column"):
205
+ output_dataframe = gr.DataFrame(
206
+ headers=["Index", "白背圖URL", "參考圖URL", "提示詞"],
207
+ col_count=(4, "fixed"),
208
+ interactive=False,
209
+ label="已處理的試算表數據"
210
+ )
211
+
212
+ # 新增顯示白背圖和參考圖的欄位
213
+ with gr.Row():
214
+ original_white_back_output = gr.Image(label="原始白背圖", elem_classes="output-gallery", interactive=False)
215
+ original_ref_image_output = gr.Image(label="原始參考圖", elem_classes="output-gallery", interactive=False)
216
+
217
+ generated_image_output = gr.Image(label="生成的圖片", elem_classes="output-gallery", interactive=False)
218
+ operation_log_output = gr.Textbox(
219
+ label="操作日誌",
220
+ lines=10,
221
+ placeholder="文字回應和日誌會顯示在這裡。"
222
+ )
223
+
224
+ # 按鈕的事件綁定
225
+ process_button.click(
226
+ fn=process_sheet_data,
227
+ inputs=[sheet_url_input],
228
+ outputs=[output_dataframe, operation_log_output]
229
+ )
230
+
231
+ generate_selected_button.click(
232
+ fn=generate_image_from_row,
233
+ inputs=[sheet_url_input, row_index_input, gemini_api_key],
234
+ outputs=[original_white_back_output, original_ref_image_output, generated_image_output, operation_log_output]
235
+ )
236
+
237
+ demo.queue().launch(mcp_server=True, share=True)