Files
ewoooc/services/gemini_service.py
2026-05-21 14:45:32 +08:00

493 lines
17 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Google Gemini AI 服務模組
負責與 Google Gemini API 互動,提供文案生成、關鍵字提取等功能
支援 gemini-1.5-flash, gemini-2.5-flash, gemini-2.5-pro 模型
"""
import os
import time
import logging
from typing import Optional, Dict, Any, List
from dataclasses import dataclass, field
from datetime import date
from services.gemini_guard import (
gemini_disabled_message,
get_gemini_api_key,
is_gemini_fallback_enabled,
)
logger = logging.getLogger(__name__)
# Gemini 設定 - 支援環境變數覆蓋API key 一律透過 gemini_guard 取得
DEFAULT_GEMINI_MODEL = os.getenv('GEMINI_MODEL', 'gemini-1.5-flash')
GEMINI_TIMEOUT = int(os.getenv('GEMINI_TIMEOUT', '60')) # 秒
# Gemini 定價 (USD per 1M tokens) - 2024/2025 定價
GEMINI_PRICING = {
'gemini-1.5-flash': {'input': 0.075, 'output': 0.30},
'gemini-1.5-flash-latest': {'input': 0.075, 'output': 0.30},
'gemini-1.5-pro': {'input': 1.25, 'output': 5.00},
'gemini-1.5-pro-latest': {'input': 1.25, 'output': 5.00},
'gemini-2.5-flash': {'input': 0.10, 'output': 0.40},
'gemini-2.5-flash-preview-05-20': {'input': 0.10, 'output': 0.40},
'gemini-2.5-pro': {'input': 1.25, 'output': 10.00},
'gemini-2.5-pro-preview-05-06': {'input': 1.25, 'output': 10.00},
}
# 可選模型清單(供 UI 選擇)
AVAILABLE_GEMINI_MODELS = [
{'id': 'gemini-1.5-flash', 'name': 'Gemini 1.5 Flash', 'description': '最便宜、速度快'},
{'id': 'gemini-2.5-flash', 'name': 'Gemini 2.5 Flash', 'description': '性價比最佳'},
{'id': 'gemini-2.5-pro', 'name': 'Gemini 2.5 Pro', 'description': '最強大、高品質'},
]
@dataclass
class GeminiResponse:
"""Gemini 回應結構"""
success: bool
content: str
model: str
error: Optional[str] = None
total_duration: Optional[float] = None
input_tokens: int = 0
output_tokens: int = 0
total_tokens: int = 0
input_cost: float = 0.0
output_cost: float = 0.0
total_cost: float = 0.0
class GeminiService:
"""Google Gemini AI 服務"""
# 連線狀態快取
_connection_cache = {'status': None, 'timestamp': 0, 'models': []}
_CACHE_TTL = 300 # 快取 5 分鐘
def __init__(self, api_key: str = None, model: str = None):
"""
初始化 Gemini 服務
Args:
api_key: Google API Key預設從環境變數取得
model: 預設模型(預設 gemini-1.5-flash
"""
self.api_key = api_key or get_gemini_api_key("gemini_service")
self.model = model or DEFAULT_GEMINI_MODEL
self._client = None
self._initialized = False
def _ensure_initialized(self) -> bool:
"""確保 SDK 已初始化"""
if self._initialized:
return True
if not is_gemini_fallback_enabled("gemini_service"):
logger.info(gemini_disabled_message("gemini_service"))
return False
if not self.api_key:
self.api_key = get_gemini_api_key("gemini_service")
if not self.api_key:
logger.warning("Gemini API Key 未設定")
return False
try:
import google.generativeai as genai
genai.configure(api_key=self.api_key)
self._initialized = True
logger.info("Gemini SDK 初始化成功")
return True
except ImportError:
logger.error("請安裝 google-generativeai: pip install google-generativeai")
return False
except Exception as e:
logger.error(f"Gemini SDK 初始化失敗: {e}")
return False
def check_connection(self) -> bool:
"""檢查 Gemini 服務是否可用(含快取)"""
now = time.time()
# 使用快取避免頻繁檢查
if (GeminiService._connection_cache['status'] is not None and
now - GeminiService._connection_cache['timestamp'] < GeminiService._CACHE_TTL):
return GeminiService._connection_cache['status']
if not self._ensure_initialized():
GeminiService._connection_cache = {'status': False, 'timestamp': now, 'models': []}
return False
try:
import google.generativeai as genai
# 嘗試列出模型驗證連線
models = list(genai.list_models())
available_models = [m.name for m in models if 'generateContent' in m.supported_generation_methods]
logger.info(f"Gemini 連線成功,可用模型數: {len(available_models)}")
GeminiService._connection_cache = {
'status': True,
'timestamp': now,
'models': available_models
}
return True
except Exception as e:
logger.error(f"Gemini 連線失敗: {e}")
GeminiService._connection_cache = {'status': False, 'timestamp': now, 'models': []}
return False
def list_models(self) -> List[Dict]:
"""列出可用模型(供 UI 選擇)"""
return AVAILABLE_GEMINI_MODELS
@staticmethod
def calculate_cost(model: str, input_tokens: int, output_tokens: int) -> Dict[str, float]:
"""
計算 API 費用 (USD)
Args:
model: 模型名稱
input_tokens: 輸入 token 數量
output_tokens: 輸出 token 數量
Returns:
dict: {'input_cost': float, 'output_cost': float, 'total_cost': float}
"""
# 查找定價(支援完整模型名稱和簡短名稱)
pricing = None
for model_key, price in GEMINI_PRICING.items():
if model_key in model or model in model_key:
pricing = price
break
if not pricing:
# 使用預設定價(最便宜的)
pricing = GEMINI_PRICING.get('gemini-1.5-flash', {'input': 0.075, 'output': 0.30})
# 計算費用per 1M tokens
input_cost = (input_tokens / 1_000_000) * pricing['input']
output_cost = (output_tokens / 1_000_000) * pricing['output']
total_cost = input_cost + output_cost
return {
'input_cost': round(input_cost, 6),
'output_cost': round(output_cost, 6),
'total_cost': round(total_cost, 6)
}
def generate(self, prompt: str, model: str = None,
system_prompt: str = None, temperature: float = 0.7,
timeout: int = None) -> GeminiResponse:
"""
生成文字
Args:
prompt: 使用者提示
model: 模型名稱(預設使用 self.model
system_prompt: 系統提示
temperature: 創意度 (0-2)
timeout: 自訂超時時間(秒)
Returns:
GeminiResponse
"""
model_name = model or self.model
request_timeout = timeout or GEMINI_TIMEOUT
if not is_gemini_fallback_enabled("gemini_service.generate"):
return GeminiResponse(
success=False,
content='',
model=model_name,
error=gemini_disabled_message("gemini_service.generate")
)
if not self._ensure_initialized():
return GeminiResponse(
success=False,
content='',
model=model_name,
error="Gemini SDK 未初始化,請檢查 API Key"
)
try:
import google.generativeai as genai
start_time = time.time()
# 建立模型
generation_config = genai.types.GenerationConfig(
temperature=temperature,
max_output_tokens=2048,
)
gen_model = genai.GenerativeModel(
model_name=model_name,
generation_config=generation_config,
system_instruction=system_prompt if system_prompt else None
)
logger.info(f"[Gemini] 開始生成,模型: {model_name},超時: {request_timeout}")
# 生成內容
response = gen_model.generate_content(
prompt,
request_options={'timeout': request_timeout}
)
end_time = time.time()
duration = end_time - start_time
# 解析回應
content = response.text if response.text else ''
# 取得 token 用量
input_tokens = 0
output_tokens = 0
if hasattr(response, 'usage_metadata'):
input_tokens = getattr(response.usage_metadata, 'prompt_token_count', 0)
output_tokens = getattr(response.usage_metadata, 'candidates_token_count', 0)
# 計算費用
costs = self.calculate_cost(model_name, input_tokens, output_tokens)
logger.info(f"[Gemini] 生成完成,耗時: {duration:.2f}秒,"
f"tokens: {input_tokens}+{output_tokens}={input_tokens+output_tokens}"
f"費用: ${costs['total_cost']:.6f}")
return GeminiResponse(
success=True,
content=content,
model=model_name,
total_duration=duration,
input_tokens=input_tokens,
output_tokens=output_tokens,
total_tokens=input_tokens + output_tokens,
input_cost=costs['input_cost'],
output_cost=costs['output_cost'],
total_cost=costs['total_cost']
)
except Exception as e:
logger.error(f"Gemini 生成錯誤: {e}")
return GeminiResponse(
success=False,
content='',
model=model_name,
error=str(e)
)
def generate_sales_copy(self, product_name: str, trend_keywords: List[str] = None,
style: str = "吸睛", upcoming_holidays: List[Dict] = None,
bestseller_products: List[Dict] = None,
model: str = None) -> GeminiResponse:
"""
生成銷售文案
Args:
product_name: 商品名稱
trend_keywords: 相關趨勢關鍵字
style: 文案風格 (吸睛/專業/溫馨/急迫)
upcoming_holidays: 即將到來的假期
bestseller_products: 競品熱銷商品
model: 指定模型(預設使用 self.model
Returns:
GeminiResponse
"""
style_prompts = {
"吸睛": "使用吸引眼球的標題和表情符號",
"專業": "使用專業術語,強調成分和功效",
"溫馨": "使用溫暖的語氣,強調呵護和關愛",
"急迫": "使用限時優惠的語氣,創造緊迫感"
}
# 趨勢關鍵字
trend_context = ""
if trend_keywords:
trend_context = f"\n目前的熱門趨勢關鍵字:{', '.join(trend_keywords)}。請嘗試將這些趨勢融入文案中。"
# 即將到來的假期
holiday_context = ""
if upcoming_holidays:
holidays_text = []
for h in upcoming_holidays[:3]:
name = h.get('name', '')
days = h.get('days_until', 0)
if days == 0:
holidays_text.append(f"{name}(今天)")
elif days == 1:
holidays_text.append(f"{name}(明天)")
else:
holidays_text.append(f"{name}{days}天後)")
if holidays_text:
holiday_context = f"\n即將到來的假期:{', '.join(holidays_text)}。可以考慮結合節慶氛圍或送禮情境。"
# 競品熱銷參考
bestseller_context = ""
if bestseller_products:
products_text = [f"{p.get('name', '')}${p.get('price', '')}" for p in bestseller_products[:3]]
if products_text:
bestseller_context = f"\n市場熱銷參考:{', '.join(products_text)}。可參考熱銷趨勢但要突出自家商品特色。"
system_prompt = """你是一位專業的電商銷售文案寫手和行銷策略專家,專門為台灣電商平台撰寫商品文案。
你的文案特點:
- 使用繁體中文
- 善用表情符號增加吸引力
- 強調商品賣點和消費者利益
- 適時使用行動呼籲 (CTA)
- 若有即將到來的節日,可適度融入節慶元素
- 提供完整的行銷建議"""
prompt = f"""請為以下商品撰寫完整的銷售文案套組:
商品名稱:{product_name}
文案風格:{style_prompts.get(style, style_prompts['吸睛'])}
{trend_context}{holiday_context}{bestseller_context}
請按照以下格式生成完整的銷售文案套組:
【大標題】
15字以內的主打標語吸引眼球適合用於廣告Banner
【中標題】
30字以內的副標題補充說明賣點
【小標題】
20字以內的精簡標語適合用於社群貼文
【詳細文案】
100-150字的完整銷售文案包含商品特色、使用情境、行動呼籲
【推廣建議】
• 社群推廣Facebook/Instagram/LINE 等社群平台的建議策略)
• 影音內容:(短影音/直播/開箱影片等建議)
• 其他建議EDM、部落格、KOL合作等專業建議
請確保所有內容使用繁體中文,風格一致,並突出商品價值:"""
return self.generate(
prompt,
model=model,
system_prompt=system_prompt,
temperature=0.8,
timeout=90
)
def extract_keywords(self, text: str, max_keywords: int = 10,
model: str = None) -> GeminiResponse:
"""
從文字中提取關鍵字
Args:
text: 要分析的文字
max_keywords: 最大關鍵字數量
model: 指定模型
Returns:
GeminiResponsecontent 為逗號分隔的關鍵字)
"""
system_prompt = "你是一位關鍵字提取專家。請從給定的文字中提取最重要的關鍵字。"
prompt = f"""請從以下文字中提取最多 {max_keywords} 個關鍵字,這些關鍵字應該能代表文章的主題和重點。
文字內容:
{text}
請只輸出關鍵字,用逗號分隔,不要輸出其他內容:"""
return self.generate(prompt, model=model, system_prompt=system_prompt, temperature=0.3)
def search_product_insights(self, product_name: str,
include_competitors: bool = True,
include_trends: bool = True,
model: str = None) -> GeminiResponse:
"""
搜尋商品相關的市場洞察
Args:
product_name: 商品名稱
include_competitors: 是否包含競品分析
include_trends: 是否包含趨勢分析
model: 指定模型
Returns:
GeminiResponse
"""
system_prompt = """你是一位資深的電商市場分析師,專精於台灣市場。
你擅長分析商品的市場定位、競爭對手、以及銷售趨勢。
請提供全面但簡潔的市場洞察,使用繁體中文。"""
analysis_parts = ["市場定位分析"]
if include_competitors:
analysis_parts.append("主要競爭對手分析")
if include_trends:
analysis_parts.append("市場趨勢分析")
competitors_json = '"competitors": [{"name": "競品名稱", "strength": "優勢", "weakness": "劣勢"}],' if include_competitors else ""
trends_json = '"trends": {"current": "當前趨勢", "forecast": "趨勢預測", "seasonality": "季節性因素"},' if include_trends else ""
analysis_list = '\n'.join([f'{i+1}. {part}' for i, part in enumerate(analysis_parts)])
prompt = f"""請為以下商品提供市場洞察分析:
商品名稱:{product_name}
請分析以下面向:
{analysis_list}
請用以下 JSON 格式回覆(務必輸出有效的 JSON
{{
"product_name": "{product_name}",
"market_position": {{
"target_audience": "目標客群描述",
"price_range": "價格區間建議",
"positioning": "市場定位建議"
}},
{competitors_json}
{trends_json}
"recommendations": ["銷售建議1", "銷售建議2", "銷售建議3"],
"keywords": ["行銷關鍵字1", "關鍵字2", "關鍵字3"]
}}"""
return self.generate(prompt, model=model, system_prompt=system_prompt, temperature=0.6, timeout=90)
# 建立全域服務實例
gemini_service = GeminiService()
if __name__ == "__main__":
# 測試程式碼
logging.basicConfig(level=logging.INFO)
service = GeminiService()
# 測試連線
print("測試 Gemini 連線...")
if service.check_connection():
print(f"連線成功!")
# 測試文案生成
print("\n測試文案生成...")
result = service.generate_sales_copy(
"玻尿酸保濕面膜",
trend_keywords=["換季保養", "敏感肌"],
style="吸睛"
)
if result.success:
print(f"生成結果:\n{result.content}")
print(f"\n耗時: {result.total_duration:.2f}")
print(f"Token 用量: 輸入 {result.input_tokens}, 輸出 {result.output_tokens}")
print(f"費用: ${result.total_cost:.6f} USD")
else:
print(f"生成失敗: {result.error}")
else:
print("連線失敗(請確認 GEMINI_API_KEY 環境變數)")