AutoLS_2 / app.py
davidlee831117's picture
Create app.py
27735e2 verified
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)