140 lines
5.6 KiB
Python
140 lines
5.6 KiB
Python
|
|
"""
|
|||
|
|
图片生成服务 - 使用 Gemini API 生图,存储到本地/云托管
|
|||
|
|
"""
|
|||
|
|
import httpx
|
|||
|
|
import base64
|
|||
|
|
import time
|
|||
|
|
import hashlib
|
|||
|
|
import os
|
|||
|
|
from typing import Optional
|
|||
|
|
|
|||
|
|
from app.config import get_settings
|
|||
|
|
|
|||
|
|
# 图片尺寸规范(基于前端展示尺寸 × 3倍清晰度)
|
|||
|
|
IMAGE_SIZES = {
|
|||
|
|
"cover": {"width": 240, "height": 330, "desc": "竖版封面图,3:4比例"},
|
|||
|
|
"avatar": {"width": 150, "height": 150, "desc": "正方形头像,1:1比例"},
|
|||
|
|
"background": {"width": 1120, "height": 840, "desc": "横版背景图,4:3比例"},
|
|||
|
|
"character": {"width": 512, "height": 768, "desc": "竖版角色立绘,2:3比例,透明背景"}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
class ImageGenService:
|
|||
|
|
def __init__(self):
|
|||
|
|
settings = get_settings()
|
|||
|
|
self.api_key = settings.gemini_api_key
|
|||
|
|
self.base_url = "https://work.poloapi.com/v1beta"
|
|||
|
|
# 计算绝对路径
|
|||
|
|
base_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '../..', settings.upload_path))
|
|||
|
|
self.upload_dir = os.path.join(base_dir, "images")
|
|||
|
|
os.makedirs(self.upload_dir, exist_ok=True)
|
|||
|
|
|
|||
|
|
def get_size_prompt(self, image_type: str) -> str:
|
|||
|
|
"""获取尺寸描述提示词"""
|
|||
|
|
size = IMAGE_SIZES.get(image_type, IMAGE_SIZES["background"])
|
|||
|
|
return f"Image size: {size['width']}x{size['height']} pixels, {size['desc']}."
|
|||
|
|
|
|||
|
|
async def generate_image(self, prompt: str, image_type: str = "background", style: str = "anime") -> Optional[dict]:
|
|||
|
|
"""
|
|||
|
|
调用 Gemini 生成图片
|
|||
|
|
prompt: 图片描述
|
|||
|
|
image_type: 图片类型(cover/avatar/background/character)
|
|||
|
|
style: 风格(anime/realistic/illustration)
|
|||
|
|
"""
|
|||
|
|
style_prefix = {
|
|||
|
|
"anime": "anime style, high quality illustration, vibrant colors, ",
|
|||
|
|
"realistic": "photorealistic, high detail, cinematic lighting, ",
|
|||
|
|
"illustration": "digital art illustration, beautiful artwork, "
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# 组合完整提示词:风格 + 尺寸 + 内容
|
|||
|
|
size_prompt = self.get_size_prompt(image_type)
|
|||
|
|
full_prompt = f"{style_prefix.get(style, '')}{size_prompt} {prompt}"
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
async with httpx.AsyncClient(timeout=60.0) as client:
|
|||
|
|
response = await client.post(
|
|||
|
|
f"{self.base_url}/models/gemini-3-pro-image-preview:generateContent",
|
|||
|
|
headers={
|
|||
|
|
"Content-Type": "application/json",
|
|||
|
|
"x-goog-api-key": self.api_key
|
|||
|
|
},
|
|||
|
|
json={
|
|||
|
|
"contents": [{
|
|||
|
|
"parts": [{
|
|||
|
|
"text": f"Generate an image: {full_prompt}"
|
|||
|
|
}]
|
|||
|
|
}],
|
|||
|
|
"generationConfig": {
|
|||
|
|
"responseModalities": ["TEXT", "IMAGE"]
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
if response.status_code == 200:
|
|||
|
|
data = response.json()
|
|||
|
|
candidates = data.get("candidates", [])
|
|||
|
|
if candidates:
|
|||
|
|
parts = candidates[0].get("content", {}).get("parts", [])
|
|||
|
|
for part in parts:
|
|||
|
|
if "inlineData" in part:
|
|||
|
|
return {
|
|||
|
|
"success": True,
|
|||
|
|
"image_data": part["inlineData"]["data"],
|
|||
|
|
"mime_type": part["inlineData"].get("mimeType", "image/png")
|
|||
|
|
}
|
|||
|
|
return {"success": False, "error": "No image in response"}
|
|||
|
|
else:
|
|||
|
|
error_text = response.text[:200]
|
|||
|
|
return {"success": False, "error": f"API error: {response.status_code} - {error_text}"}
|
|||
|
|
except Exception as e:
|
|||
|
|
return {"success": False, "error": str(e)}
|
|||
|
|
|
|||
|
|
async def save_image(self, image_data: str, filename: str) -> Optional[str]:
|
|||
|
|
"""保存图片到本地,返回访问URL"""
|
|||
|
|
try:
|
|||
|
|
image_bytes = base64.b64decode(image_data)
|
|||
|
|
file_path = os.path.join(self.upload_dir, filename)
|
|||
|
|
|
|||
|
|
with open(file_path, "wb") as f:
|
|||
|
|
f.write(image_bytes)
|
|||
|
|
|
|||
|
|
# 返回可访问的URL路径
|
|||
|
|
return f"/uploads/images/{filename}"
|
|||
|
|
except Exception as e:
|
|||
|
|
print(f"Save error: {e}")
|
|||
|
|
return None
|
|||
|
|
|
|||
|
|
async def generate_and_save(self, prompt: str, image_type: str = "background", style: str = "anime") -> dict:
|
|||
|
|
"""生成图片并保存"""
|
|||
|
|
result = await self.generate_image(prompt, image_type, style)
|
|||
|
|
|
|||
|
|
if not result or not result.get("success"):
|
|||
|
|
return {
|
|||
|
|
"success": False,
|
|||
|
|
"error": result.get("error", "生成失败") if result else "生成失败"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# 生成文件名
|
|||
|
|
timestamp = int(time.time() * 1000)
|
|||
|
|
hash_str = hashlib.md5(prompt.encode()).hexdigest()[:8]
|
|||
|
|
ext = "png" if "png" in result.get("mime_type", "") else "jpg"
|
|||
|
|
filename = f"{image_type}_{timestamp}_{hash_str}.{ext}"
|
|||
|
|
|
|||
|
|
url = await self.save_image(result["image_data"], filename)
|
|||
|
|
|
|||
|
|
if url:
|
|||
|
|
return {"success": True, "url": url, "filename": filename}
|
|||
|
|
else:
|
|||
|
|
return {"success": False, "error": "保存失败"}
|
|||
|
|
|
|||
|
|
|
|||
|
|
# 延迟初始化单例
|
|||
|
|
_service_instance = None
|
|||
|
|
|
|||
|
|
def get_image_gen_service():
|
|||
|
|
global _service_instance
|
|||
|
|
if _service_instance is None:
|
|||
|
|
_service_instance = ImageGenService()
|
|||
|
|
return _service_instance
|