Spaces:
Sleeping
Sleeping
File size: 10,755 Bytes
27735e2 |
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 235 236 237 |
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) |