"""海报图片生成器 — 支持多供应商生图模型(OpenAI / 豆包 / 智谱等)。""" import os import base64 import logging logger = logging.getLogger(__name__) # 用户尺寸 → API 尺寸映射 SIZE_MAP = { "1080x1920": "1024x1792", "900x500": "1792x1024", "1080x1080": "1024x1024", "800x1200": "1024x1792", } def _load_image_config() -> dict: """从数据库读取海报图片模型配置。""" try: from insurance.db.compat import db from insurance.models.system_setting import SystemSetting keys = ["poster_image_provider", "poster_image_model", "poster_image_api_key", "poster_image_base_url"] settings = {s.key: s.value for s in SystemSetting.query.filter(SystemSetting.key.in_(keys)).all()} return settings except Exception: return {} class PosterImageGenerator: """海报图片生成器,支持 OpenAI 兼容接口的多供应商生图模型。""" def __init__(self): self._client = None self._config = None def _get_config(self) -> dict: if self._config is None: self._config = _load_image_config() return self._config def _get_client(self): if self._client is None: import openai cfg = self._get_config() api_key = cfg.get("poster_image_api_key") or os.getenv("OPENAI_API_KEY", "") base_url = cfg.get("poster_image_base_url") or os.getenv("OPENAI_BASE_URL", "https://api.openai.com/v1") self._client = openai.OpenAI(api_key=api_key, base_url=base_url) return self._client def _get_model(self) -> str: cfg = self._get_config() return cfg.get("poster_image_model") or "gpt-image-1" def build_prompt(self, template: dict = None, product: dict = None, company: dict = None, copy: dict = None, size: str = "1024x1792") -> str: """组装完整的生图 prompt。""" parts = [ "你是一位专业的保险营销海报设计师。请根据以下信息生成一张保险营销海报。", "", ] # 风格要求 if template: parts.append("【风格要求】") parts.append(template.get("styleDescription", "")) if template.get("colorScheme"): parts.append(f"配色方案:{template['colorScheme']}") parts.append("") # 产品信息 if product: parts.append("【产品信息】") parts.append(f"产品名称:{product.get('displayName', '')}") parts.append(f"所属公司:{company.get('displayName', '') if company else ''}") if product.get("manualParsedRules"): rules = product["manualParsedRules"] if isinstance(rules, str): import json try: rules = json.loads(rules) except Exception: rules = {} features = rules.get("features", []) if features: parts.append("产品亮点:" + ";".join(f.get("title", "") for f in features[:3])) parts.append("") # 营销文案 if copy: parts.append("【营销文案】") if copy.get("headline"): parts.append(f"标题:{copy['headline']}") if copy.get("body"): parts.append(f"正文:{copy['body']}") if copy.get("call_to_action"): parts.append(f"行动号召:{copy['call_to_action']}") parts.append("") # 输出要求 parts.append("【输出要求】") parts.append(f"- 尺寸:{size} 像素") parts.append("- 文字清晰可读,中文为主") parts.append("- 符合保险行业专业风格") parts.append("- 包含公司 Logo 位置(如有)") return "\n".join(parts) def generate(self, prompt: str, size: str = "1024x1792", reference_image: str = None) -> bytes: """调用配置的生图模型生成海报图片。 返回: PNG 图片的 bytes """ api_size = SIZE_MAP.get(size, "1024x1792") client = self._get_client() model = self._get_model() kwargs = { "model": model, "prompt": prompt, "n": 1, "size": api_size, } # 如果有参考图,尝试传入(仅允许 uploads 目录下的文件) uploads_root = os.path.abspath("uploads") if reference_image and os.path.exists(reference_image): ref_abs = os.path.abspath(reference_image) if not ref_abs.startswith(uploads_root): logger.warning(f"参考图路径不在允许目录: {reference_image}") reference_image = None try: with open(reference_image, "rb") as img_file: response = client.images.edit( model=model, image=img_file, prompt=prompt, n=1, size=api_size, ) image_base64 = response.data[0].b64_json return base64.b64decode(image_base64) except Exception as e: logger.warning(f"参考图 edit 失败,降级为纯 prompt 生成: {e}") response = client.images.generate(**kwargs) image_base64 = response.data[0].b64_json return base64.b64decode(image_base64) def generate_fallback(copy_content: dict, size: str = "1024x1792") -> bytes: """降级方案:使用 Pillow 生成基础排版图。""" from PIL import Image, ImageDraw, ImageFont w, h = 1024, 1792 if "x" in size: try: parts = size.split("x") w, h = int(parts[0]), int(parts[1]) except Exception: pass # 缩放到 API 支持的尺寸 w, h = min(w, 1792), min(h, 1792) img = Image.new("RGB", (w, h), color=(26, 26, 46)) draw = ImageDraw.Draw(img) # 简单排版 headline = copy_content.get("headline", "") body = copy_content.get("body", "") cta = copy_content.get("call_to_action", "") # 尝试加载支持中文的字体 chinese_fonts = [ "/usr/share/fonts/truetype/noto/NotoSansCJK-Regular.ttc", "/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc", "/usr/share/fonts/noto-cjk/NotoSansCJK-Regular.ttc", "/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc", "/usr/share/fonts/wqy-zenhei/wqy-zenhei.ttc", "C:/Windows/Fonts/msyh.ttc", # Windows 微软雅黑 "C:/Windows/Fonts/simsun.ttc", # Windows 宋体 ] font_large = font_medium = font_small = None for font_path in chinese_fonts: if os.path.exists(font_path): try: font_large = ImageFont.truetype(font_path, 48) font_medium = ImageFont.truetype(font_path, 28) font_small = ImageFont.truetype(font_path, 22) break except Exception: continue if font_large is None: font_large = ImageFont.load_default() font_medium = ImageFont.load_default() font_small = ImageFont.load_default() # 绘制文字 y = h // 4 if headline: draw.text((w // 2, y), headline, fill="white", font=font_large, anchor="mm") y += 80 if body: # 简单换行 for line in body.split("\n")[:4]: draw.text((w // 2, y), line, fill=(200, 200, 200), font=font_medium, anchor="mm") y += 40 if cta: draw.text((w // 2, h - 200), cta, fill=(102, 126, 234), font=font_small, anchor="mm") import io buf = io.BytesIO() img.save(buf, format="PNG") return buf.getvalue()