"""阿里云百炼图片生成服务""" import os from dataclasses import dataclass from typing import Optional import httpx # 平台尺寸适配 PLATFORM_IMAGE_SPECS = { "zhihu": { "cover": {"width": 690, "height": 280, "ratio": "2.5:1"}, "inline": {"width": 500, "height": 375, "ratio": "4:3"}, }, "wechat": { "cover": {"width": 900, "height": 383, "ratio": "2.35:1"}, "inline": {"width": 800, "height": 600, "ratio": "4:3"}, }, "xiaohongshu": { "cover": {"width": 1080, "height": 1080, "ratio": "1:1"}, # 方版 "inline": {"width": 1242, "height": 1660, "ratio": "3:4"}, # 竖版 }, "toutiao": { "cover": {"width": 1024, "height": 678, "ratio": "1.5:1"}, }, "baijiahao": { "cover": {"width": 600, "height": 400, "ratio": "3:2"}, }, "weibo": { "cover": {"width": 980, "height": 560, "ratio": "1.75:1"}, }, "bilibili": { "cover": {"width": 1920, "height": 1080, "ratio": "16:9"}, }, "jianshu": { "cover": {"width": 800, "height": 600, "ratio": "4:3"}, }, "juejin": { "cover": {"width": 1024, "height": 768, "ratio": "4:3"}, }, "douyin": { "cover": {"width": 1080, "height": 1920, "ratio": "9:16"}, # 竖版短视频封面 }, } # 风格选项 IMAGE_STYLES = { "modern": {"name": "现代简约", "prompt": "modern minimalist style, clean design, professional"}, "tech": {"name": "科技感", "prompt": "tech style, futuristic, digital, blue tones"}, "elegant": {"name": "优雅商务", "prompt": "elegant business style, sophisticated, premium"}, "creative": {"name": "创意活力", "prompt": "creative vibrant style, colorful, dynamic"}, "minimal": {"name": "极简主义", "prompt": "ultra minimal, white space, typography focus"}, } # 排版选项 LAYOUT_OPTIONS = { "centered": {"name": "居中排版", "prompt": "centered composition, text in middle"}, "left_text": {"name": "左文右图", "prompt": "left side text, right side visual"}, "top_text": {"name": "上文下图", "prompt": "text on top, visual below"}, "text_overlay": {"name": "文字叠加", "prompt": "text overlay on background image"}, } @dataclass class ImageResult: """图片生成结果""" url: str width: int height: int prompt: str platform: str task_id: str class ImageGenerationError(Exception): """图片生成异常""" pass class ImageGenerator: """阿里云百炼图片生成服务(万相-文生图V1)""" def __init__(self): self.api_key = os.getenv("ALIYUN_DASHSCOPE_API_KEY") self.base_url = "https://dashscope.aliyuncs.com/api/v1/services/aigc/text2image/image-synthesis" self.timeout = 120.0 # 异步任务等待超时时间(秒) async def generate_cover( self, title: str, platform: str, image_type: str = "cover", style: str = "modern", layout: str = "centered", custom_prompt: str = None, ) -> ImageResult: """生成封面图 Args: title: 文章标题 platform: 目标平台 image_type: 图片类型 (cover/inline) style: 风格选项 layout: 排版选项 custom_prompt: 自定义提示词(可选) Returns: ImageResult: 包含生成结果的 dataclass """ # 1. 获取平台尺寸 specs = PLATFORM_IMAGE_SPECS.get(platform, PLATFORM_IMAGE_SPECS["zhihu"]) size_spec = specs.get(image_type, specs["cover"]) # 2. 构建提示词 if custom_prompt: prompt = custom_prompt else: prompt = self._build_prompt(title, platform, style, layout) # 3. 调用百炼API(异步) task_id = await self._create_task(prompt, size_spec) # 4. 轮询结果 result = await self._wait_for_result(task_id) return ImageResult( url=result["image_url"], width=size_spec["width"], height=size_spec["height"], prompt=prompt, platform=platform, task_id=task_id, ) def _build_prompt(self, title: str, platform: str, style: str, layout: str) -> str: """构建AI提示词 Args: title: 文章标题 platform: 目标平台 style: 风格选项 layout: 排版选项 Returns: str: 构造的英文提示词 """ style_prompt = IMAGE_STYLES.get(style, IMAGE_STYLES["modern"])["prompt"] layout_prompt = LAYOUT_OPTIONS.get(layout, LAYOUT_OPTIONS["centered"])["prompt"] # 平台特定要求 platform_notes = { "xiaohongshu": "warm tones, lifestyle, lifestyle photography", "wechat": "professional, clean, suitable for WeChat article cover", "zhihu": "intellectual, professional, suitable for long-form content", "toutiao": "eye-catching, clear hierarchy, news style", "baijiahao": "clear, professional, suitable for news media", "weibo": "social media friendly, engaging", "bilibili": "anime style friendly, vibrant, suitable for video platform", "jianshu": "literary, elegant, clean layout", "juejin": "tech blog style, developer friendly", "douyin": "vertical video style, eye-catching, short form content", } platform_note = platform_notes.get(platform, "") return f"{title}, {style_prompt}, {layout_prompt}, {platform_note}, high quality, 4K" async def _create_task(self, prompt: str, size_spec: dict) -> str: """创建异步任务 Args: prompt: 英文提示词 size_spec: 尺寸规格 Returns: str: 任务ID Raises: ImageGenerationError: API调用失败时 """ if not self.api_key: raise ImageGenerationError("ALIYUN_DASHSCOPE_API_KEY 未设置") headers = { "Authorization": f"Bearer {self.api_key}", "Content-Type": "application/json", } payload = { "model": "wanx-v1", # 万相-文生图V1 "input": { "prompt": prompt, }, "parameters": { "size": f"{size_spec['width']}x{size_spec['height']}", "n": 1, }, } async with httpx.AsyncClient(timeout=30.0) as client: try: response = await client.post( self.base_url, headers=headers, json=payload, ) response.raise_for_status() data = response.json() if data.get("code"): raise ImageGenerationError(f"API错误: {data.get('message', data.get('code'))}") # 返回任务ID task_id = data.get("output", {}).get("task_id") if not task_id: raise ImageGenerationError("未获取到任务ID") return task_id except httpx.HTTPStatusError as e: raise ImageGenerationError(f"HTTP错误: {e.response.status_code}") except httpx.RequestError as e: raise ImageGenerationError(f"请求错误: {e}") async def _wait_for_result(self, task_id: str) -> dict: """轮询等待结果 Args: task_id: 任务ID Returns: dict: 包含 image_url 的结果 Raises: ImageGenerationError: 任务失败或超时 """ status_url = f"{self.base_url}/task/{task_id}" headers = { "Authorization": f"Bearer {self.api_key}", } async with httpx.AsyncClient(timeout=self.timeout) as client: import asyncio start_time = asyncio.get_event_loop().time() poll_interval = 2.0 # 轮询间隔(秒) while True: try: response = await client.get(status_url, headers=headers) response.raise_for_status() data = response.json() status = data.get("output", {}).get("task_status") if status == "succeeded": # 任务成功 images = data.get("output", {}).get("results", []) if images: return {"image_url": images[0].get("url")} raise ImageGenerationError("未获取到生成图片URL") elif status == "failed": error_msg = data.get("output", {}).get("message", "任务失败") raise ImageGenerationError(f"图片生成失败: {error_msg}") elif status == "pending": # 等待中,继续轮询 pass else: # 未知状态,继续等待 pass # 检查超时 elapsed = asyncio.get_event_loop().time() - start_time if elapsed > self.timeout: raise ImageGenerationError("图片生成超时") # 等待后继续轮询 await asyncio.sleep(poll_interval) except httpx.HTTPStatusError as e: raise ImageGenerationError(f"HTTP错误: {e.response.status_code}") except httpx.RequestError as e: raise ImageGenerationError(f"请求错误: {e}") @staticmethod def get_platform_specs(platform: str) -> Optional[dict]: """获取平台的图片规格 Args: platform: 平台标识 Returns: Optional[dict]: 平台图片规格,如果没有则返回None """ return PLATFORM_IMAGE_SPECS.get(platform) @staticmethod def get_supported_platforms() -> list[str]: """获取支持的平台列表""" return list(PLATFORM_IMAGE_SPECS.keys()) @staticmethod def get_styles() -> dict: """获取所有风格选项""" return IMAGE_STYLES @staticmethod def get_layouts() -> dict: """获取所有排版选项""" return LAYOUT_OPTIONS