Spaces:
Sleeping
Sleeping
| 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 | |
| # --- 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) | |
| # 增加 read_csv 的 robustness,防止某些 Google Sheet 導致的 ParserError | |
| 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) | |
| # 檢查是否至少有 3 列 (白背圖URL, 參考圖URL, 提示詞),因為索引會在程式中生成 | |
| if df.shape[1] < 3: | |
| error_msg = f"錯誤:Google Sheet 至少需要 3 列 (白背圖URL, 參考圖URL, 提示詞)。目前只有 {df.shape[1]} 列。" | |
| raise gr.Error(error_msg) | |
| data_list = [] | |
| for i, row in df.iterrows(): | |
| # 確保有足夠的列且至少第一列(白背圖URL)不為空才加入 | |
| if len(row) >= 3 and pd.notna(row.iloc[0]): | |
| 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) | |
| # Gradio顯示的行數從2開始,DataFrame索引從0開始,所以需要減2 | |
| row_index = int(row_number) - 2 | |
| if row_index < 0 or row_index >= df.shape[0]: | |
| raise gr.Error(f"指定的行數 {row_number} 不存在。") | |
| row_data = df.iloc[row_index] | |
| # 確保取值時索引不越界,並處理 NaN | |
| white_back_image_url = row_data.iloc[0] if len(row_data) > 0 and pd.notna(row_data.iloc[0]) else "" | |
| ref_image_url = row_data.iloc[1] if len(row_data) > 1 and pd.notna(row_data.iloc[1]) else "" | |
| prompt_text = row_data.iloc[2] if len(row_data) > 2 and 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) or not url.strip(): # 增加對空字串的檢查 | |
| 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 if white_back_url else '無'}\n" | |
| log_message += f"參考圖URL: {ref_image_url if ref_image_url else '無'}\n" | |
| log_message += f"提示詞: {prompt_text if prompt_text else '無'}\n" | |
| images_for_gemini = [] | |
| # 下載圖片 | |
| wb_img = None | |
| if white_back_url: | |
| wb_img = load_image_from_url(white_back_url) | |
| if wb_img: | |
| images_for_gemini.append(wb_img) | |
| else: | |
| log_message += f"警告:無法下載白背圖 '{white_back_url}'。\n" | |
| ref_img = None | |
| if ref_image_url: | |
| ref_img = load_image_from_url(ref_image_url) | |
| if ref_img: | |
| images_for_gemini.append(ref_img) | |
| else: | |
| log_message += f"警告:無法下載參考圖 '{ref_image_url}'。\n" | |
| if not images_for_gemini: | |
| # 如果沒有任何圖片,但有提示詞,嘗試純文字生成 | |
| if prompt_text: | |
| log_message += "沒有可用的輸入圖片,嘗試僅使用提示詞生成圖片。\n" | |
| else: | |
| return None, None, None, "警告:無效的圖片 URL 且無提示詞,無法生成圖片。", log_message | |
| 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 wb_img, ref_img, image_path, log_message | |
| else: | |
| log_message += "圖片生成失敗。\n" | |
| log_message += f"Gemini 文字回應: {text_response}" | |
| return wb_img, ref_img, None, log_message | |
| except ValueError: | |
| return None, None, None, "行數必須為數字。", "生成圖片時發生錯誤:行數必須為數字。" | |
| except Exception as e: | |
| return None, None, None, f"生成圖片時發生錯誤: {e}", 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", | |
| # 從環境變數讀取預設值 (如果存在) | |
| value=os.environ.get("GEMINI_API_KEY", "") | |
| ) | |
| 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="已處理的試算表數據" | |
| ) | |
| # 新增顯示白背圖和參考圖的欄位 | |
| with gr.Row(): | |
| original_white_back_output = gr.Image(label="原始白背圖", elem_classes="output-gallery", interactive=False) | |
| original_ref_image_output = gr.Image(label="原始參考圖", elem_classes="output-gallery", interactive=False) | |
| generated_image_output = gr.Image(label="生成的圖片", elem_classes="output-gallery", interactive=False) | |
| 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=[original_white_back_output, original_ref_image_output, generated_image_output, operation_log_output] | |
| ) | |
| demo.queue().launch(mcp_server=True, share=True) |