Skill: yh22e/beauty-image (version 3.2.0)

--- name: beauty-image description: 商业化AI图片生成助手。40+场景模板, 30+风格词典, 智能意图识别, 结构化Prompt, 三引擎智能路由 (Wanx/Seedream4/Seedream5)。 version: 3.2.0 author: 圆规 metadata: openclaw: requires: env: - DASHSCOPE_API_KEY - ARK_API_KEY - DEEPSEEK_API_KEY bins: - uv primaryEnv: DASHSCOPE_API_KEY emoji: "🎨" homepage: https://github.com/xyva-yuangui/beauty-image os: ["darwin", "linux", "win32"] --- # Beauty Image V3 — 商业化AI图片生成 AI 图片生成助手。**触发后必须先询问用户确认**，再执行生成。 V3升级: 意图识别(40+场景) + 结构化Prompt + 风格词典(30+) + 智能引擎路由 | 触发场景 | 询问确认内容 | |----------|-------------| | 名片设计 | 姓名、职称、公司、风格(赛博朋克/极简/金属) | | 海报创作 | 主题、配色、排版风格、文字元素 | | 头像/IP | 人物描述、风格、表情、背景 | | 产品摄影 | 产品描述、背景、布光偏好 | | 3D/材质 | 主体、材质(水晶/毛绒/充气/乐高)、场景 | | 通用图片 | 画面描述、尺寸比例、风格、引擎选择 | **执行流程**: 触发 → 意图识别 → 模板匹配 → 询问缺失信息 → Prompt构建 → 引擎路由 → 生成 → 展示 ## Usage (V3 脚本, 推荐) ### 智能模式 (自动识别意图+模板+路由) ```bash uv run {baseDir}/scripts/generate_image_v3.py --prompt "帮我做一张赛博朋克风格的个人名片" uv run {baseDir}/scripts/generate_image_v3.py --prompt "画一只猫的水晶质感手办" uv run {baseDir}/scripts/generate_image_v3.py --prompt "上海天气卡片" uv run {baseDir}/scripts/generate_image_v3.py --prompt "做个毛绒版的苹果logo" ``` ### 指定场景模板 ```bash uv run {baseDir}/scripts/generate_image_v3.py --prompt "猫" --scene 3d_crystal uv run {baseDir}/scripts/generate_image_v3.py --prompt "武士" --scene art_ukiyoe_card --style 浮世绘 uv run {baseDir}/scripts/generate_image_v3.py --prompt "咖啡店" --scene 3d_miniature ``` ### 指定字段 (JSON) ```bash uv run {baseDir}/scripts/generate_image_v3.py --prompt "名片" --scene biz_card --fields '{"name":"圆规","title":"CEO","company":"XyvaClaw"}' ``` ### Raw模式 (兼容V2, 直接传prompt) ```bash uv run {baseDir}/scripts/generate_image_v3.py --prompt "一幅美丽的风景画" --raw ``` ### 指定尺寸/比例 ```bash uv run {baseDir}/scripts/generate_image_v3.py --prompt "城市全景" --size 全景 uv run {baseDir}/scripts/generate_image_v3.py --prompt "手机壁纸" --size 9:16 uv run {baseDir}/scripts/generate_image_v3.py --prompt "小红书配图" --size 小红书 uv run {baseDir}/scripts/generate_image_v3.py --prompt "电影海报" --size 海报 ``` ### 列出模板/风格/引擎/尺寸 ```bash uv run {baseDir}/scripts/generate_image_v3.py --list-scenes uv run {baseDir}/scripts/generate_image_v3.py --list-styles uv run {baseDir}/scripts/generate_image_v3.py --list-providers uv run {baseDir}/scripts/generate_image_v3.py --list-sizes ``` ## 尺寸和比例 (15种 + 中文别名) 支持 15 种宽高比，可用比例名、中文别名或直接像素值指定。 | 比例 | wanx 尺寸 | 中文别名 | |------|----------|---------| | 1:1 | 1280×1280 | 正方, 头像 | | 3:4 | 1104×1472 | 小红书, A4 | | 4:3 | 1472×1104 | — | | 9:16 | 960×1696 | 竖版, 手机壁纸 | | 16:9 | 1696×960 | 横版, PPT, 名片 | | 2:3 | 1048×1568 | 海报 | | 3:2 | 1568×1048 | — | | 1:2 | 904×1816 | — | | 2:1 | 1808×912 | — | | 9:21 | 840×1960 | 竖屏, 长图 | | 21:9 | 1952×840 | 宽屏, 电影 | | 1:3 | 744×2224 | 长海报 | | 3:1 | 2216×744 | 横幅, banner | | 1:4 | 640×2560 | 超长, 滚动长图 | | 4:1 | 2560×640 | 全景, 超宽 | Seedream 引擎支持 2K/3K 高分辨率输出。 ## 场景模板 (26+) | 分类 | 模板ID | 名称 | 推荐引擎 | |------|--------|------|----------| | 商务 | `biz_card` | 名片设计 | seedream5 | | 商务 | `biz_poster` | 商业海报 | seedream5 | | 商务 | `biz_product` | 产品摄影 | seedream4 | | 社交 | `social_avatar` | 头像/IP | seedream4 | | 社交 | `social_emoji` | 表情包 | wanx | | 社交 | `social_card` | 金句卡片 | wanx | | 创意 | `art_general` | 艺术创作 | seedream5 | | 创意 | `art_ukiyoe_card` | 浮世绘闪卡 | seedream5 | | 3D | `3d_crystal` | 水晶质感 | seedream5 | | 3D | `3d_plush` | 毛绒玩具 | seedream4 | | 3D | `3d_inflatable` | 充气玩具 | seedream4 | | 3D | `3d_miniature` | 微缩房间 | seedream5 | | 实用 | `util_weather` | 天气卡片 | seedream5 | | 实用 | `util_fridge_magnet` | 冰箱贴 | seedream5 | | 实用 | `util_food_map` | 美食地图 | seedream5 | | 实用 | `util_storyboard` | 电影分镜 | seedream5 | | 设计 | `design_ppt` | PPT页面 | seedream5 | | 设计 | `design_logo` | Logo | wanx | | 设计 | `design_packaging` | 包装设计 | seedream5 | | 人物 | `person_portrait` | 写实肖像 | seedream5 | | 人物 | `person_action_figure` | 手办 | seedream5 | | 场景 | `scene_chalkboard` | 黑板粉笔画 | seedream4 | | 电商 | `ecom_main` | 电商主图 | wanx | | 电商 | `ecom_banner` | 电商横幅 | wanx | | 内容 | `content_xhs` | 小红书配图 | seedream4 | | 内容 | `content_blog` | 文章头图 | wanx | ## 风格词典 (20+) 赛博朋克, 浮世绘, 水墨画, 吉卜力, 像素风, 油画, 漫画, 波普艺术, 清明上河图, 水晶, 毛绒, 充气, 乐高, 黏土, 电影感, 产品摄影, 街拍, 胶片, 等距微缩, 3D渲染 ## 引擎选择指南 | 引擎 | 适用场景 | 画质 | 速度 | 费用 | |------|---------|------|------|------| | **seedream5** | 最高画质，组图 | ⭐⭐⭐⭐⭐ | 中 | 中 | | **seedream4** | 高画质，性价比 | ⭐⭐⭐⭐ | 较快 | 低 | | **wanx** | 文字渲染好 | ⭐⭐⭐ | 快 | 低 | V3自动按场景+风格路由引擎, 无需手动选择。 ## API Key 配置 (首次使用必读) 本技能需要至少配置 **一个** API Key 才能使用。推荐配置通义万相 (免费额度较多)。 ### 方式一: 环境变量 (推荐) ```bash # 通义万相 (wanx引擎, 必选其一) export DASHSCOPE_API_KEY=sk-xxxxxxxxxxxxxxxx # 火山引擎 (seedream4/seedream5引擎, 可选, 画质更高) export ARK_API_KEY=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx # DeepSeek (可选, 仅用于 --use-llm 深度意图解析) export DEEPSEEK_API_KEY=sk-xxxxxxxxxxxxxxxx ``` ### 方式二: openclaw.json 配置 在 `~/.openclaw/openclaw.json` 中添加: ```json { "models": { "providers": { "wanxiang": { "apiKey": "sk-your-dashscope-key" }, "ark": { "apiKey": "your-ark-api-key" }, "deepseek": { "apiKey": "sk-your-deepseek-key" } } } } ``` ### API Key 获取地址 | 引擎 | Key名称 | 获取地址 | 说明 | |------|---------|---------|------| | **通义万相** | `DASHSCOPE_API_KEY` | https://dashscope.console.aliyun.com/apiKey | 阿里云百炼平台, 注册后有免费额度 | | **Seedream** | `ARK_API_KEY` | https://console.volcengine.com/ark/region:ark+cn-beijing/apiKey | 火山引擎, 需开通方舟平台 | | **DeepSeek** | `DEEPSEEK_API_KEY` | https://platform.deepseek.com/api_keys | 可选, 用于LLM意图解析 | ### 健康检查 ```bash uv run {baseDir}/scripts/check.py ``` ## Workflow 1. 执行 generate_image_v3.py (优先) 或 generate_image_v2.py (V2兼容) 2. 解析输出中 `MEDIA_URL:` 开头的行 3. 用 markdown 语法展示图片: `![Generated Image](URL)` 4. 如有多张图, 逐一展示所有 `MEDIA_URL:` 行 5. 不下载图片，除非用户要求保存 ## Legacy V2脚本 `generate_image_v2.py` 仍可使用 (无模板/无意图识别): ```bash uv run {baseDir}/scripts/generate_image_v2.py --prompt "描述" --provider auto ``` V1脚本 `generate_image.py` 仅支持万相: ```bash uv run {baseDir}/scripts/generate_image.py --prompt "描述" --model wan2.6-t2i ``` ## Notes - V3自动识别场景并匹配模板, 构建结构化prompt - 支持 `--use-llm` 启用DeepSeek意图解析 (低置信度时) - 图片 URL 有效期 24 小时，请及时下载保存 - Seedream 5.0 lite 支持组图 (≤15张)

path: README.md
content: # Beauty Image V3 — 商业化AI图片生成 OpenClaw Skill

[![OpenClaw Skill](https://img.shields.io/badge/OpenClaw-Skill-blue)](https://clawhub.ai)
[![License: MIT-0](https://img.shields.io/badge/License-MIT--0-yellow.svg)](LICENSE)

> **作者: 圆规** | [GitHub](https://github.com/xyva-yuangui/beauty-image)

一个功能强大的 AI 图片生成 OpenClaw 技能，支持 **40+ 场景模板**、**30+ 风格词典**、智能意图识别和三引擎智能路由。

## 特性

- **智能意图识别**: 从自然语言自动解析场景、风格、字段
- **40+ 场景模板**: 名片/海报/头像/产品/水晶/毛绒/天气卡片/分镜/信息图...
- **30+ 风格词典**: 赛博朋克/水墨/浮世绘/吉卜力/像素风/液态玻璃/全息镭射...
- **三引擎智能路由**: 自动按场景+风格选择最优引擎
  - **通义万相 (wanx)** — 阿里云, 文字渲染佳, 成本低
  - **Seedream 4.0** — 火山引擎, 高画质, 性价比
  - **Seedream 5.0** — 火山引擎, 最高画质
- **结构化 Prompt**: 6 层结构 (核心→主体→风格→光照→构图→技术)
- **Prompt 美化器**: 简单输入自动升级为专业级提示词
- **跨平台**: macOS / Linux / Windows

## 安装

### 通过 ClawHub 安装 (推荐)

在 OpenClaw 中搜索 `beauty-image` 并安装。

### 手动安装

```bash
git clone https://github.com/xyva-yuangui/beauty-image.git
# 将仓库复制到 ~/.openclaw/workspace/skills/beauty-image
```

## API Key 配置 (必须)

本技能需要至少配置 **一个** API Key 才能使用。

### 快速开始

```bash
# 方式一: 环境变量 (推荐)
export DASHSCOPE_API_KEY=sk-xxxxxxxxxxxxxxxx        # 通义万相 (必选其一)
export ARK_API_KEY=xxxxxxxx-xxxx-xxxx-xxxx-xxxx      # Seedream (可选, 画质更高)
export DEEPSEEK_API_KEY=sk-xxxxxxxxxxxxxxxx          # DeepSeek (可选, LLM意图解析)
```

### openclaw.json 配置

在 `~/.openclaw/openclaw.json` 中添加:

```json
{
  "models": {
    "providers": {
      "wanxiang": { "apiKey": "sk-your-dashscope-key" },
      "ark": { "apiKey": "your-ark-api-key" },
      "deepseek": { "apiKey": "sk-your-deepseek-key" }
    }
  }
}
```

### API Key 获取地址

| 引擎 | 环境变量 | 获取地址 | 说明 |
|------|---------|---------|------|
| **通义万相** | `DASHSCOPE_API_KEY` | [阿里云百炼](https://dashscope.console.aliyun.com/apiKey) | 注册后有免费额度, 推荐首选 |
| **Seedream** | `ARK_API_KEY` | [火山引擎方舟](https://console.volcengine.com/ark/region:ark+cn-beijing/apiKey) | 需开通方舟平台 |
| **DeepSeek** | `DEEPSEEK_API_KEY` | [DeepSeek](https://platform.deepseek.com/api_keys) | 可选, 用于 `--use-llm` |

### 健康检查

```bash
uv run scripts/check.py
```

## 使用示例

### 智能模式 (自动识别意图 + 模板 + 路由)

```bash
uv run scripts/generate_image_v3.py --prompt "帮我做一张赛博朋克风格的个人名片"
uv run scripts/generate_image_v3.py --prompt "画一只猫的水晶质感手办"
uv run scripts/generate_image_v3.py --prompt "上海天气卡片"
uv run scripts/generate_image_v3.py --prompt "做个毛绒版的苹果logo"
```

### 指定场景模板

```bash
uv run scripts/generate_image_v3.py --prompt "猫" --scene 3d_crystal
uv run scripts/generate_image_v3.py --prompt "武士" --scene art_ukiyoe_card --style 浮世绘
uv run scripts/generate_image_v3.py --prompt "咖啡店" --scene 3d_miniature
```

### 指定字段 (JSON)

```bash
uv run scripts/generate_image_v3.py --prompt "名片" --scene biz_card \
  --fields '{"name":"圆规","title":"CEO","company":"XyvaClaw"}'
```

### Raw 模式 (直接传 prompt)

```bash
uv run scripts/generate_image_v3.py --prompt "一幅美丽的风景画" --raw
```

### 列出模板/风格/引擎

```bash
uv run scripts/generate_image_v3.py --list-scenes
uv run scripts/generate_image_v3.py --list-styles
uv run scripts/generate_image_v3.py --list-providers
```

## 场景模板一览

| 分类 | 模板 | 推荐引擎 |
|------|------|----------|
| 商务 | 名片设计, 商业海报, 产品摄影 | seedream5/4 |
| 社交 | 头像/IP, 表情包, 金句卡片, YouTube缩略图 | seedream4/wanx |
| 创意 | 艺术创作, 浮世绘闪卡 | seedream5 |
| 3D | 水晶质感, 毛绒玩具, 充气玩具, 微缩房间 | seedream5/4 |
| 实用 | 天气卡片, 冰箱贴, 美食地图, 电影分镜 | seedream5 |
| 设计 | PPT页面, Logo, 包装设计, App界面, 书籍封面 | seedream5/wanx |
| 摄影 | 美食摄影, 时尚大片, 风光大片 | seedream4/5 |
| 电商 | 电商主图, 电商横幅 | wanx |
| 内容 | 小红书配图, 文章头图, 漫画条, 梗图 | seedream4/wanx |
| 信息图 | Bento信息图, 对比信息图 | seedream5 |

## 引擎选择

| 引擎 | 画质 | 速度 | 成本 | 适用场景 |
|------|------|------|------|---------|
| **seedream5** | ⭐⭐⭐⭐⭐ | 中 | 中 | 最高画质, 组图 |
| **seedream4** | ⭐⭐⭐⭐ | 较快 | 低 | 高画质, 性价比 |
| **wanx** | ⭐⭐⭐ | 快 | 低 | 文字渲染好 |

V3 自动按场景+风格路由引擎, 无需手动选择。

## 项目结构

```
beauty-image/
├── SKILL.md                          # OpenClaw 技能定义
├── _meta.json                        # 技能元数据
├── README.md                         # 本文件
├── scripts/
│   ├── generate_image_v3.py          # V3 主脚本 (推荐)
│   ├── generate_image_v2.py          # V2 兼容脚本
│   ├── generate_image.py             # V1 万相专用脚本
│   ├── image_intent_parser.py        # 意图识别引擎
│   ├── image_prompt_templates.py     # 场景模板 + 风格词典 + Prompt Builder
│   └── check.py                      # 健康检查脚本
└── examples/
    └── README.md                     # 示例说明
```

## 依赖

- Python >= 3.10
- [uv](https://github.com/astral-sh/uv) (脚本运行器)
- requests >= 2.31.0 (自动安装)

## License

MIT


path: _meta.json
content: slug: beauty-image
version: 3.2.0
name: Beauty Image V3
author: 圆规
repository: https://github.com/xyva-yuangui/beauty-image
license: MIT-0
description: 商业化AI图片生成: 40+场景模板, 30+风格词典, 智能意图识别, 结构化Prompt, 三引擎智能路由 (Wanx/Seedream4/Seedream5)

path: examples/README.md
content: # Beauty Image 使用示例

## 基础示例

### 智能模式 (自动识别)

```bash
# 名片生成
uv run scripts/generate_image_v3.py --prompt "帮我做一张赛博朋克风格的个人名片，名字叫圆规"

# 水晶质感
uv run scripts/generate_image_v3.py --prompt "画一只猫的水晶质感手办"

# 天气卡片
uv run scripts/generate_image_v3.py --prompt "上海天气卡片"

# 毛绒玩具
uv run scripts/generate_image_v3.py --prompt "做个毛绒版的苹果logo"
```

### 指定场景 + 风格

```bash
# 浮世绘闪卡
uv run scripts/generate_image_v3.py --prompt "日本武士" --scene art_ukiyoe_card --style 浮世绘

# 微缩场景
uv run scripts/generate_image_v3.py --prompt "咖啡店" --scene 3d_miniature

# 带字段的名片
uv run scripts/generate_image_v3.py --prompt "名片" --scene biz_card \
  --fields '{"name":"圆规","title":"CEO","company":"XyvaClaw"}'
```

### 指定引擎

```bash
# 强制使用 seedream5 (最高画质)
uv run scripts/generate_image_v3.py --prompt "一幅油画风格的日落" --engine seedream5

# 强制使用 wanx (文字渲染佳)
uv run scripts/generate_image_v3.py --prompt "金句卡片" --engine wanx
```

## 配置说明

### 必需配置

至少配置以下 API Key 之一:

1. `DASHSCOPE_API_KEY` — 通义万相引擎 (推荐, 免费额度多)
   - 获取: https://dashscope.console.aliyun.com/apiKey
2. `ARK_API_KEY` — Seedream 引擎 (画质更高)
   - 获取: https://console.volcengine.com/ark/region:ark+cn-beijing/apiKey

### 可选配置

1. `DEEPSEEK_API_KEY` — 用于 `--use-llm` 深度意图解析
   - 获取: https://platform.deepseek.com/api_keys

## 故障排除

### 常见问题

1. **"缺少 API Key" 错误**
   - 原因: 未配置任何 API Key
   - 解决: 设置 `DASHSCOPE_API_KEY` 环境变量，或在 `~/.openclaw/openclaw.json` 中配置

2. **"wan2.6 失败, 降级到 wan2.5" 警告**
   - 原因: wan2.6 模型暂时不可用或尺寸参数不合法
   - 解决: 正常现象，脚本会自动降级到 wan2.5。如持续失败，检查 API Key 是否有效

3. **"HTTP 401" 错误**
   - 原因: API Key 无效或已过期
   - 解决: 重新获取 API Key 并配置

4. **Seedream 不可用**
   - 原因: 未配置 `ARK_API_KEY`
   - 解决: 前往火山引擎控制台获取 API Key

### 健康检查

```bash
uv run scripts/check.py
```


path: scripts/check.py
content: #!/usr/bin/env python3
"""Beauty Image 技能健康检查"""
import sys
import os
import json
from pathlib import Path

# 自动定位skill根目录 (scripts/../)
base_dir = Path(__file__).resolve().parent.parent

# 检查脚本是否存在
for script_name in ["generate_image.py", "generate_image_v2.py", "generate_image_v3.py"]:
    script_path = base_dir / "scripts" / script_name
    if script_path.exists():
        print(f"✅ {script_name}")
    else:
        print(f"❌ {script_name} 不存在：{script_path}")

# 检查 SKILL.md
skill_md = base_dir / "SKILL.md"
if not skill_md.exists():
    print(f"❌ SKILL.md 不存在：{skill_md}")
    sys.exit(1)
else:
    print("✅ SKILL.md")

# 检查 API Key 配置
print("\n--- API Key 检查 ---")

dashscope_key = os.environ.get('DASHSCOPE_API_KEY', '')
if dashscope_key:
    print("✅ DASHSCOPE_API_KEY 已配置 (通义万相)")
else:
    print("⚠️ DASHSCOPE_API_KEY 未设置")
    print("   获取地址: https://dashscope.console.aliyun.com/apiKey")

ark_key = os.environ.get('ARK_API_KEY', '') or os.environ.get('VOLC_API_KEY', '')
if ark_key:
    print("✅ ARK_API_KEY 已配置 (Seedream)")
else:
    print("⚠️ ARK_API_KEY 未设置 (Seedream引擎不可用)")
    print("   获取地址: https://console.volcengine.com/ark/region:ark+cn-beijing/apiKey")

deepseek_key = os.environ.get('DEEPSEEK_API_KEY', '')
if deepseek_key:
    print("✅ DEEPSEEK_API_KEY 已配置 (LLM意图解析)")
else:
    print("ℹ️ DEEPSEEK_API_KEY 未设置 (可选, 用于--use-llm深度意图解析)")

# 检查 openclaw.json 配置
config_path = Path.home() / ".openclaw" / "openclaw.json"
if config_path.exists():
    try:
        with open(config_path) as f:
            config = json.load(f)
        providers = config.get("models", {}).get("providers", {})
        if providers.get("wanxiang", {}).get("apiKey"):
            print("✅ openclaw.json: wanxiang provider 已配置")
        for name in ["ark", "volcengine", "doubao"]:
            if providers.get(name, {}).get("apiKey"):
                print(f"✅ openclaw.json: {name} provider 已配置")
                break
    except Exception:
        pass

if not dashscope_key and not ark_key:
    print("\n⚠️ 至少需要配置一个 API Key 才能使用图片生成功能")
    print("   推荐: 设置 DASHSCOPE_API_KEY (免费额度较多)")
else:
    print("\n✅ OK - beauty-image 就绪")

print("   支持的引擎: wanx (万相), seedream4, seedream5")
print("   支持的模型: wan2.6-t2i, wan2.5-t2i-preview, doubao-seedream-4/5")


path: scripts/generate_image.py
content: #!/usr/bin/env python3
# /// script
# requires-python = ">=3.10"
# dependencies = [
#     "requests>=2.31.0",
# ]
# ///
"""
Generate images using Beauty Image API (Alibaba Cloud DashScope).

作者: 圆规
GitHub: https://github.com/xyva-yuangui/beauty-image
License: MIT

Usage:
    uv run generate_image.py --prompt "your image description" --filename "output.png" [--model wan2.6-t2i|wan2.5-t2i-preview] [--size 1280*1280|1696*960|960*1696] [--api-key KEY]
"""

import argparse
import os
import sys
import json
import time
import base64
from pathlib import Path


def _read_openclaw_config_key() -> str | None:
    """Read API key from ~/.openclaw/openclaw.json → models.providers.wanxiang.apiKey."""
    try:
        config_path = Path.home() / ".openclaw" / "openclaw.json"
        with open(config_path) as f:
            config = json.load(f)
        key = config.get("models", {}).get("providers", {}).get("wanxiang", {}).get("apiKey")
        if key and not key.startswith("sk-sp-"):
            return key
    except Exception:
        pass
    return None


def get_api_key(provided_key: str | None) -> str | None:
    """Get API key from provided arg, env var, or openclaw config.
    
    查找顺序:
      1. --api-key 参数 (跳过 sk-sp- 前缀的百炼 Key)
      2. 环境变量 DASHSCOPE_API_KEY
      3. ~/.openclaw/openclaw.json 配置
    
    获取地址: https://dashscope.console.aliyun.com/apiKey
    """
    if provided_key and not provided_key.startswith("sk-sp-"):
        return provided_key
    if provided_key and provided_key.startswith("sk-sp-"):
        print(f"⚠️ 忽略传入的百炼Key (sk-sp- 不支持万相图片生成)")
    
    env_key = os.environ.get("DASHSCOPE_API_KEY")
    if env_key:
        return env_key
    
    return _read_openclaw_config_key()


def main():
    parser = argparse.ArgumentParser(
        description="Generate images using Beauty Image API"
    )
    parser.add_argument(
        "--prompt", "-p",
        required=True,
        help="Image description/prompt"
    )
    parser.add_argument(
        "--filename", "-f",
        help="Output filename (optional, if not provided will only return URL)"
    )
    parser.add_argument(
        "--model", "-m",
        choices=[
            # 万相 2.6 系列（支持同步调用，推荐）
            "wan2.6-t2i",           # 最新版，支持自定义分辨率
            # 万相 2.5 及以下（异步调用）
            "wan2.5-t2i-preview",   # 预览版
            "wan2.2-t2i-flash",     # 极速版，性价比之王
            "wan2.2-t2i-plus",      # 专业版
            "wanx2.1-t2i-turbo",    # 2.1 极速版
            "wanx2.1-t2i-plus",     # 2.1 专业版
        ],
        default="wan2.6-t2i",
        help="Model to use: wan2.6-t2i (recommended), wan2.2-t2i-flash (best value)"
    )
    parser.add_argument(
        "--size", "-s",
        choices=["1664*928", "1024*1024", "720*1280", "1280*720"],
        default="1664*928",
        help="Output size (default: 1664*928 for 16:9 ratio)"
    )
    parser.add_argument(
        "--negative-prompt", "-n",
        default="低分辨率，低画质，肢体畸形，手指畸形，画面过饱和，蜡像感，人脸无细节，过度光滑，画面具有AI感。构图混乱。文字模糊，扭曲。",
        help="Negative prompt to avoid unwanted elements"
    )
    parser.add_argument(
        "--no-prompt-extend",
        action="store_true",
        help="Disable automatic prompt enhancement"
    )
    parser.add_argument(
        "--watermark",
        action="store_true",
        help="Add watermark to generated image"
    )
    parser.add_argument(
        "--api-key", "-k",
        help="DashScope API key (overrides DASHSCOPE_API_KEY env var)"
    )
    parser.add_argument(
        "--no-verify-ssl",
        action="store_true",
        help="Disable SSL certificate verification (use with caution)"
    )

    args = parser.parse_args()

    # Get API key
    [REDACTED_SECRET])
    if not [REDACTED_SECRET]"Error: 缺少通义万相 API Key (DASHSCOPE_API_KEY)", file=sys.stderr)
        print("请通过以下方式之一配置:", file=sys.stderr)
        print("  1. 设置环境变量: export DASHSCOPE_API_KEY=your-key", file=sys.stderr)
        print("  2. 传入参数: --api-key your-key", file=sys.stderr)
        print("  3. 在 ~/.openclaw/openclaw.json 中配置: models.providers.wanxiang.apiKey", file=sys.stderr)
        print("获取地址: https://dashscope.console.aliyun.com/apiKey", file=sys.stderr)
        sys.exit(1)

    # Import here after checking API key
    import requests

    # Set up output path

    print(f"Generating image with {args.model}...")
    print(f"Size: {args.size}")
    print(f"Prompt: {args.prompt}")

    # Determine API endpoint and build payload based on model series
    # wan2.6 uses synchronous multimodal-generation API (new protocol)
    # wan2.5 and below use asynchronous text2image API (old protocol)
    if args.model.startswith("wan2.6"):
        # Wanx 2.6 - Synchronous API (new protocol)
        api_url = "https://dashscope.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation"
        payload = {
            "model": args.model,
            "input": {
                "messages": [
                    {
                        "role": "user",
                        "content": [
                            {
                                "text": args.prompt
                            }
                        ]
                    }
                ]
            },
            "parameters": {
                "negative_prompt": args.negative_prompt,
                "prompt_extend": not args.no_prompt_extend,
                "watermark": args.watermark,
                "size": args.size,
                "n": 1  # Default to 1 image for testing
            }
        }
        headers = {
            "Content-Type": "application/json",
            "Authorization": f"Bearer {api_key}"
        }
    else:
        # Wanx 2.5 and below - Asynchronous API (old protocol)
        api_url = "https://dashscope.aliyuncs.com/api/v1/services/aigc/text2image/image-synthesis"
        payload = {
            "model": args.model,
            "input": {
                "prompt": args.prompt
            },
            "parameters": {
                "negative_prompt": args.negative_prompt,
                "prompt_extend": not args.no_prompt_extend,
                "watermark": args.watermark,
                "size": args.size,
                "n": 1  # Default to 1 image for testing
            }
        }
        headers = {
            "Content-Type": "application/json",
            "Authorization": f"Bearer {api_key}",
            "X-DashScope-Async": "enable"  # Required for async API
        }
    
    try:
        # Make API request
        response = requests.post(
            api_url,
            headers=headers,
            json=payload,
            timeout=120,
            verify=True
        )

        response.raise_for_status()
        result = response.json()

        # Check for errors
        if result.get("code"):
            error_msg = result.get("message", "Unknown error")
            print(f"API Error: {error_msg}", file=sys.stderr)
            sys.exit(1)

        # Handle async API (wan2.5 and below) - need to poll for result
        if args.model.startswith("wan2.6"):
            # Synchronous API (wan2.6) - direct response with image URL
            output_data = result.get("output", {})
            choices = output_data.get("choices", [])
            
            if not choices:
                print("Error: No choices in response", file=sys.stderr)
                print(f"Response: {json.dumps(result, indent=2, ensure_ascii=False)}", file=sys.stderr)
                sys.exit(1)

            # Get image URL from first choice
            message = choices[0].get("message", {})
            content = message.get("content", [])
            
            if not content or not content[0].get("image"):
                print("Error: No image URL in response", file=sys.stderr)
                print(f"Response: {json.dumps(result, indent=2, ensure_ascii=False)}", file=sys.stderr)
                sys.exit(1)

            image_url = content[0]["image"]
            print(f"\nImage URL: {image_url}")
        else:
            # Asynchronous API (wan2.5 and below) - get task_id and poll
            output_data = result.get("output", {})
            task_id = output_data.get("task_id")
            
            if not task_id:
                print("Error: No task_id in response", file=sys.stderr)
                print(f"Response: {json.dumps(result, indent=2, ensure_ascii=False)}", file=sys.stderr)
                sys.exit(1)
            
            print(f"Task ID: {task_id}")
            print("Polling for result...")
            
            # Poll for task result
            image_url = None
            max_attempts = 30
            poll_interval = 10  # seconds
            
            for attempt in range(max_attempts):
                time.sleep(poll_interval)
                
                # Query task status
                task_url = f"https://dashscope.aliyuncs.com/api/v1/tasks/{task_id}"
                task_response = requests.get(
                    task_url,
                    headers={"Authorization": f"Bearer {api_key}"},
                    timeout=30,
                    verify=True
                )
                task_response.raise_for_status()
                task_result = task_response.json()
                
                task_output = task_result.get("output", {})
                task_status = task_output.get("task_status", "UNKNOWN")
                
                print(f"Attempt {attempt + 1}/{max_attempts}: Status = {task_status}")
                
                if task_status == "SUCCEEDED":
                    # Extract image URL from result
                    # Async API uses "results" array, sync API uses "choices"
                    results = task_output.get("results", [])
                    if results and results[0].get("url"):
                        image_url = results[0]["url"]
                        print(f"\nImage URL: {image_url}")
                        break
                    
                    # Fallback to sync API format
                    choices = task_output.get("choices", [])
                    if choices:
                        message = choices[0].get("message", {})
                        content = message.get("content", [])
                        if content and content[0].get("image"):
                            image_url = content[0]["image"]
                            print(f"\nImage URL: {image_url}")
                            break
                    
                    print("Error: Task succeeded but no image URL found", file=sys.stderr)
                    print(f"Response: {json.dumps(task_result, indent=2, ensure_ascii=False)}", file=sys.stderr)
                    sys.exit(1)
                elif task_status == "FAILED":
                    print(f"Task failed: {task_result.get('message', 'Unknown error')}", file=sys.stderr)
                    sys.exit(1)
                elif task_status in ["PENDING", "RUNNING"]:
                    continue
                else:
                    print(f"Unknown task status: {task_status}", file=sys.stderr)
                    sys.exit(1)
            
            if not image_url:
                print("Error: Task timed out", file=sys.stderr)
                sys.exit(1)

        # If filename is provided, download and save the image
        if args.filename:
            output_path = Path(args.filename)
            output_path.parent.mkdir(parents=True, exist_ok=True)
            
            print("Downloading image...")
            img_response = requests.get(image_url, timeout=30, verify=not args.no_verify_ssl)
            img_response.raise_for_status()

            # Save the image
            with open(output_path, "wb") as f:
                f.write(img_response.content)

            full_path = output_path.resolve()
            print(f"Image saved: {full_path}")
            # Clawdbot parses MEDIA tokens and will attach the file on supported providers.
            print(f"MEDIA: {full_path}")
        else:
            # Just return the URL for Clawdbot to display
            print(f"MEDIA_URL: {image_url}")

    except requests.exceptions.HTTPError as e:
        print(f"HTTP Error: {e}", file=sys.stderr)
        try:
            error_detail = response.json()
            print(f"Error details: {json.dumps(error_detail, indent=2, ensure_ascii=False)}", file=sys.stderr)
        except:
            print(f"Response text: {response.text}", file=sys.stderr)
        sys.exit(1)
    except requests.exceptions.RequestException as e:
        print(f"Error making API request: {e}", file=sys.stderr)
        sys.exit(1)
    except Exception as e:
        print(f"Error generating image: {e}", file=sys.stderr)
        sys.exit(1)


if __name__ == "__main__":
    main()


path: scripts/generate_image_v2.py
content: #!/usr/bin/env python3
# /// script
# requires-python = ">=3.10"
# dependencies = [
#     "requests>=2.31.0",
# ]
# ///
"""
V10.2 Multi-Provider Image Generator — 多源图片生成器 (性价比优化)

作者: 圆规
GitHub: https://github.com/xyva-yuangui/beauty-image
License: MIT

支持:
  1. 通义万相 (Wanx 2.6) — 阿里云 DashScope (成本最低, 通用)
  2. doubao-seedream-4.0 — 火山引擎 ARK (性价比最佳)
  3. doubao-seedream-5.0-lite — 火山引擎 ARK (最高画质, 成本最高)

性价比排序: wanx(经济) < seedream4(均衡) < seedream5(旗舰)

auto模式智能选择:
  - standard质量 → wanx (成本最低)
  - high质量 → seedream4 (质量/成本最佳平衡)
  - 显式指定 seedream5 → seedream5 (旗舰画质)

用法:
  uv run generate_image_v2.py --prompt "描述" [--provider auto|wanx|seedream5|seedream4] [--quality high|standard]
  uv run generate_image_v2.py --prompt "描述" --provider seedream5 --size 3K --quality high
  uv run generate_image_v2.py --prompt "描述" --provider seedream5 --sequential auto --max-images 4
"""

import argparse
import base64
import json
import os
import sys
import time
from pathlib import Path

# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# API Key 管理
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

def get_wanx_key() -> str | None:
    """获取通义万相 API Key

    查找顺序:
      1. 环境变量 DASHSCOPE_API_KEY
      2. ~/.openclaw/openclaw.json → models.providers.wanxiang.apiKey
    
    注意: Bailian Coding Plan key (sk-sp-) 不支持万相图片生成API, 会自动跳过。
    获取地址: https://dashscope.console.aliyun.com/apiKey
    """
    key = os.environ.get("DASHSCOPE_API_KEY")
    if key:
        return key
    try:
        config_path = Path.home() / ".openclaw" / "openclaw.json"
        with open(config_path) as f:
            config = json.load(f)
        key = config.get("models", {}).get("providers", {}).get("wanxiang", {}).get("apiKey")
        if key and not key.startswith("sk-sp-"):
            return key
    except Exception:
        pass
    return None


def get_ark_key() -> str | None:
    """获取火山引擎 ARK API Key (Seedream 模型)

    查找顺序:
      1. 环境变量 ARK_API_KEY 或 VOLC_API_KEY
      2. ~/.openclaw/openclaw.json → models.providers.ark.apiKey

    获取地址: https://console.volcengine.com/ark/region:ark+cn-beijing/apiKey
    """
    key = os.environ.get("ARK_API_KEY") or os.environ.get("VOLC_API_KEY")
    if key:
        return key
    try:
        config_path = Path.home() / ".openclaw" / "openclaw.json"
        with open(config_path) as f:
            config = json.load(f)
        for provider_name in ["ark", "volcengine", "doubao"]:
            key = config.get("models", {}).get("providers", {}).get(provider_name, {}).get("apiKey")
            if key:
                return key
    except Exception:
        pass
    return None


# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# 质量优化 Prompt 增强
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

QUALITY_ENHANCERS = {
    "high": {
        "prefix": "",
        "suffix_zh": "，高清细腻，光影层次丰富，构图精美，色彩和谐，专业摄影级画质，8K超高清",
        "suffix_en": ", ultra high definition, rich lighting and shadows, exquisite composition, harmonious colors, professional photography quality, 8K UHD",
        "negative": "低分辨率，低画质，肢体畸形，手指畸形，画面过饱和，蜡像感，人脸无细节，过度光滑，AI感明显，构图混乱，文字模糊扭曲，噪点，模糊，过曝，欠曝",
    },
    "standard": {
        "prefix": "",
        "suffix_zh": "，画面清晰，构图合理",
        "suffix_en": ", clear image, good composition",
        "negative": "低分辨率，低画质，肢体畸形，手指畸形，蜡像感，构图混乱，文字模糊",
    },
}

SIZE_PRESETS = {
    # Seedream 5.0 lite / 4.5 / 4.0
    "2K": "2048x2048",
    "2K_16:9": "2848x1600",
    "2K_9:16": "1600x2848",
    "2K_4:3": "2304x1728",
    "2K_3:4": "1728x2304",
    "3K": "3072x3072",
    "3K_16:9": "4096x2304",
    "3K_9:16": "2304x4096",
    "3K_4:3": "3456x2592",
    "3K_3:4": "2592x3456",
    # Wanx sizes (use * separator)
    "wanx_16:9": "1664*928",
    "wanx_1:1": "1024*1024",
    "wanx_9:16": "720*1280",
}


def enhance_prompt(prompt: str, quality: str = "high") -> str:
    """增强 prompt 以提高图片质量"""
    enhancer = QUALITY_ENHANCERS.get(quality, QUALITY_ENHANCERS["standard"])
    # 检测语言
    has_chinese = any('\u4e00' <= c <= '\u9fff' for c in prompt)
    suffix = enhancer["suffix_zh"] if has_chinese else enhancer["suffix_en"]
    # 避免重复追加
    if "8K" in prompt or "高清" in prompt or "UHD" in prompt:
        return prompt
    return prompt + suffix


def resolve_size(size_input: str, provider: str) -> str:
    """解析尺寸参数"""
    if size_input in SIZE_PRESETS:
        resolved = SIZE_PRESETS[size_input]
    else:
        resolved = size_input

    # Wanx 使用 * 分隔符
    if provider == "wanx":
        return resolved.replace("x", "*")
    # Seedream 使用 x 分隔符
    return resolved.replace("*", "x")


# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# Provider: 通义万相 (Wanx 2.6)
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

def generate_wanx(prompt: str, size: str, quality: str, filename: str | None,
                  no_prompt_extend: bool, watermark: bool) -> dict:
    """通义万相图片生成"""
    import requests

    [REDACTED_SECRET])
    if not [REDACTED_SECRET] {
            "success": False,
            "error": (
                "缺少通义万相 API Key (DASHSCOPE_API_KEY)。\n"
                "请通过以下方式之一配置:\n"
                "  1. 设置环境变量: export DASHSCOPE_API_KEY=your-key\n"
                "  2. 在 ~/.openclaw/openclaw.json 中配置: models.providers.wanxiang.apiKey\n"
                "获取地址: https://dashscope.console.aliyun.com/apiKey"
            ),
        }

    enhanced = enhance_prompt(prompt, quality)
    negative = QUALITY_ENHANCERS[quality]["negative"]
    size_str = resolve_size(size, "wanx")

    # ── 优先: wan2.6-t2i 同步调用 (新协议) ──
    model_26 = "wan2.6-t2i"
    print(f"🎨 [万相 {model_26}] 同步生成中...")
    print(f"  Prompt: {enhanced[:80]}...")
    print(f"  Size: {size_str}")

    payload_26 = {
        "model": model_26,
        "input": {"messages": [{"role": "user", "content": [{"text": enhanced}]}]},
        "parameters": {
            "negative_prompt": negative,
            "prompt_extend": not no_prompt_extend,
            "watermark": watermark,
            "size": size_str,
            "n": 1,
        },
    }

    try:
        resp = requests.post(
            "https://dashscope.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation",
            headers={"Content-Type": "application/json", "Authorization": f"Bearer {api_key}"},
            json=payload_26, timeout=120,
        )
        if resp.status_code == 200:
            result = resp.json()
            choices = result.get("output", {}).get("choices", [])
            if choices:
                content = choices[0].get("message", {}).get("content", [])
                if content and content[0].get("image"):
                    return _finalize(content[0]["image"], filename, model_26, requests)
            return {"success": False, "error": f"wan2.6响应无图片: {result}"}

        err_msg = ""
        try:
            err_msg = resp.json().get("message", resp.text[:200])
        except Exception:
            err_msg = resp.text[:200]
        print(f"  ⚠️ wan2.6失败 ({resp.status_code}: {err_msg}), 降级到wan2.5...")
    except Exception as e:
        print(f"  ⚠️ wan2.6异常 ({e}), 降级到wan2.5...")

    # ── 降级: wan2.5-t2i-preview 异步调用 (旧协议) ──
    import time
    model_25 = "wan2.5-t2i-preview"
    print(f"\n🎨 [万相 {model_25}] 异步生成中...")

    payload_25 = {
        "model": model_25,
        "input": {"prompt": enhanced, "negative_prompt": negative},
        "parameters": {"size": size_str, "n": 1},
    }

    try:
        resp = requests.post(
            "https://dashscope.aliyuncs.com/api/v1/services/aigc/text2image/image-synthesis",
            headers={"Content-Type": "application/json", "Authorization": f"Bearer {api_key}", "X-DashScope-Async": "enable"},
            json=payload_25, timeout=30,
        )
        if resp.status_code != 200:
            return {"success": False, "error": f"wan2.5提交失败: {resp.json().get('message', '?')}"}

        task_id = resp.json().get("output", {}).get("task_id")
        if not task_id:
            return {"success": False, "error": "未返回task_id"}

        print(f"  任务已提交: {task_id}")
        for i in range(40):
            time.sleep(3)
            r = requests.get(
                f"https://dashscope.aliyuncs.com/api/v1/tasks/{task_id}",
                headers={"Authorization": f"Bearer {api_key}"}, timeout=15,
            )
            task_result = r.json()
            status = task_result.get("output", {}).get("task_status", "UNKNOWN")
            if i % 3 == 0:
                print(f"  等待中... ({(i+1)*3}s, 状态: {status})")
            if status == "SUCCEEDED":
                results = task_result.get("output", {}).get("results", [])
                if results and results[0].get("url"):
                    return _finalize(results[0]["url"], filename, model_25, requests)
                return {"success": False, "error": "任务成功但无图片URL"}
            elif status in ("FAILED", "UNKNOWN"):
                return {"success": False, "error": f"任务失败: {task_result.get('output', {}).get('message', '?')}"}

        return {"success": False, "error": "轮询超时 (120s)"}
    except Exception as e:
        return {"success": False, "error": str(e)}


# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# Provider: Seedream (火山引擎 ARK)
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

def generate_seedream(prompt: str, model: str, size: str, quality: str,
                      filename: str | None, watermark: bool,
                      sequential: str, max_images: int,
                      optimize_prompt_mode: str) -> dict:
    """Seedream 5.0 lite / 4.0 图片生成"""
    import requests

    [REDACTED_SECRET])
    if not [REDACTED_SECRET] {
            "success": False,
            "error": "缺少火山引擎 ARK API Key。请设置环境变量 ARK_API_KEY 或在 openclaw.json 中配置 models.providers.ark.apiKey。获取地址: https://console.volcengine.com/ark/region:ark+cn-beijing/apiKey",
        }

    enhanced = enhance_prompt(prompt, quality)
    size_str = resolve_size(size, "seedream")

    api_url = "https://ark.cn-beijing.volces.com/api/v3/images/generations"
    payload = {
        "model": model,
        "prompt": enhanced,
        "size": size_str,
        "watermark": watermark,
        "response_format": "url",
    }

    # 组图配置
    if sequential != "disabled":
        payload["sequential_image_generation"] = sequential
        if max_images > 1:
            payload["sequential_image_generation_options"] = {"max_images": max_images}

    # 提示词优化
    if model in ("doubao-seedream-5.0-lite", "doubao-seedream-4.5", "doubao-seedream-4.0"):
        payload["optimize_prompt_options"] = {"mode": optimize_prompt_mode}

    # 输出格式 (仅5.0支持)
    if model == "doubao-seedream-5.0-lite":
        payload["output_format"] = "png"

    headers = {
        "Content-Type": "application/json",
        "Authorization": f"Bearer {api_key}",
    }

    model_short = model.replace("doubao-seedream-", "SD")
    print(f"🎨 [{model_short}] 生成中...")
    print(f"  Prompt: {enhanced[:80]}...")
    print(f"  Size: {size_str}")
    if sequential != "disabled":
        print(f"  组图模式: {sequential} (max={max_images})")

    try:
        resp = requests.post(api_url, headers=headers, json=payload, timeout=180)

        if resp.status_code != 200:
            try:
                err = resp.json()
                return {"success": False, "error": f"HTTP {resp.status_code}: {err.get('error', {}).get('message', resp.text[:200])}"}
            except Exception:
                return {"success": False, "error": f"HTTP {resp.status_code}: {resp.text[:200]}"}

        result = resp.json()

        if result.get("error"):
            return {"success": False, "error": result["error"].get("message", str(result["error"]))}

        data = result.get("data", [])
        if not data:
            return {"success": False, "error": "无返回图片", "raw": result}

        # 处理多图
        images = []
        for i, item in enumerate(data):
            if item.get("url"):
                images.append({"url": item["url"], "size": item.get("size", size_str)})
            elif item.get("b64_json"):
                images.append({"b64_json": item["b64_json"][:50] + "...", "size": item.get("size", size_str)})
            elif item.get("error"):
                images.append({"error": item["error"].get("message", "未知错误")})

        if not images:
            return {"success": False, "error": "未解析到图片"}

        # 下载第一张 (如指定filename)
        first_url = images[0].get("url") if images else None
        if filename and first_url:
            return _finalize(first_url, filename, model_short, requests, extra_images=images[1:])

        # 输出usage
        usage = result.get("usage", {})

        return {
            "success": True,
            "provider": model,
            "images": images,
            "image_count": len(images),
            "usage": {
                "generated_images": usage.get("generated_images", len(images)),
                "output_tokens": usage.get("output_tokens", 0),
            },
        }

    except Exception as e:
        return {"success": False, "error": str(e)}


# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# 通用工具
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

def _finalize(image_url: str, filename: str | None, provider: str, requests_mod,
              extra_images: list | None = None) -> dict:
    """下载+保存+输出"""
    result = {"success": True, "provider": provider, "image_url": image_url}

    print(f"\n✅ 图片生成成功!")
    print(f"Image URL: {image_url}")

    if filename:
        output_path = Path(filename)
        output_path.parent.mkdir(parents=True, exist_ok=True)
        print("Downloading image...")
        img_resp = requests_mod.get(image_url, timeout=60)
        img_resp.raise_for_status()
        with open(output_path, "wb") as f:
            f.write(img_resp.content)
        full_path = output_path.resolve()
        result["local_path"] = str(full_path)
        print(f"Saved to: {full_path}")
        print(f"MEDIA: {full_path}")
    else:
        print(f"MEDIA_URL: {image_url}")

    if extra_images:
        result["extra_images"] = extra_images
        for i, img in enumerate(extra_images, 2):
            if img.get("url"):
                print(f"MEDIA_URL: {img['url']}")

    return result


# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# CLI
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

def main():
    parser = argparse.ArgumentParser(description="V10 Multi-Provider Image Generator")
    parser.add_argument("--prompt", "-p", required="--list-providers" not in sys.argv, help="图片描述")
    parser.add_argument("--provider", choices=["wanx", "seedream5", "seedream4", "auto"],
                        default="auto", help="图片生成引擎 (默认auto: 有ARK Key用seedream5, 否则wanx)")
    parser.add_argument("--size", "-s", default="2K",
                        help="尺寸: 2K, 3K, 2K_16:9, 2K_9:16, 或具体WxH如2048x2048")
    parser.add_argument("--quality", "-q", choices=["high", "standard"], default="high",
                        help="质量级别 (默认high: 8K级增强prompt)")
    parser.add_argument("--filename", "-f", help="输出文件名 (不指定则仅返回URL)")
    parser.add_argument("--no-prompt-extend", action="store_true", help="禁用平台侧prompt增强")
    parser.add_argument("--watermark", action="store_true", help="添加水印")
    parser.add_argument("--sequential", choices=["auto", "disabled"], default="disabled",
                        help="组图模式 (仅Seedream, auto=自动判断)")
    parser.add_argument("--max-images", type=int, default=1, help="最大图片数 (组图模式, 1-15)")
    parser.add_argument("--optimize-prompt", choices=["standard", "fast"], default="standard",
                        help="Seedream提示词优化模式")
    parser.add_argument("--list-providers", action="store_true", help="列出可用引擎")
    args = parser.parse_args()

    if args.list_providers:
        wanx_ok = bool(get_wanx_key())
        ark_ok = bool(get_ark_key())
        print("可用图片生成引擎:")
        print(f"  {'✅' if wanx_ok else '❌'} wanx      — 通义万相 wan2.6-t2i (阿里云)")
        print(f"  {'✅' if ark_ok else '❌'} seedream5 — doubao-seedream-5.0-lite (火山引擎, 最高画质)")
        print(f"  {'✅' if ark_ok else '❌'} seedream4 — doubao-seedream-4.0 (火山引擎)")
        if not wanx_ok:
            print("\n  💡 设置 DASHSCOPE_API_KEY 以启用万相引擎")
            print("     获取: https://dashscope.console.aliyun.com/apiKey")
        if not ark_ok:
            print("\n  💡 设置 ARK_API_KEY 以启用 Seedream 引擎")
            print("     获取: https://console.volcengine.com/ark/region:ark+cn-beijing/apiKey")
        if not wanx_ok and not ark_ok:
            print("\n  ⚠️ 至少需要配置一个 API Key 才能使用图片生成功能")
        return

    # V10.2: 性价比优化的auto选择
    provider = args.provider
    if provider == "auto":
        ark_key = get_ark_key()
        if args.quality == "high" and ark_key:
            # 高画质 + 有ARK Key → seedream4 (性价比最佳)
            provider = "seedream4"
        elif args.quality == "standard" or not ark_key:
            # 标准画质 或 无ARK Key → wanx (成本最低)
            provider = "wanx"
        else:
            provider = "seedream4"
        print(f"💡 auto选择: {provider} (quality={args.quality})")
    elif provider == "seedream5":
        print(f"💡 旗舰模式: seedream5 (最高画质, 成本较高)")

    if provider == "wanx":
        result = generate_wanx(args.prompt, args.size, args.quality, args.filename,
                               args.no_prompt_extend, args.watermark)
    elif provider == "seedream5":
        result = generate_seedream(args.prompt, "doubao-seedream-5.0-lite", args.size,
                                   args.quality, args.filename, args.watermark,
                                   args.sequential, args.max_images, args.optimize_prompt)
    elif provider == "seedream4":
        result = generate_seedream(args.prompt, "doubao-seedream-4-0-250828", args.size,
                                   args.quality, args.filename, args.watermark,
                                   args.sequential, args.max_images, args.optimize_prompt)
    else:
        print(f"未知 provider: {provider}", file=sys.stderr)
        sys.exit(1)

    if not result.get("success"):
        print(f"\n❌ 生成失败: {result.get('error', '未知错误')}", file=sys.stderr)
        sys.exit(1)

    # 输出JSON结果供程序解析
    print(f"\nRESULT_JSON: {json.dumps(result, ensure_ascii=False)}")


if __name__ == "__main__":
    main()


path: scripts/generate_image_v3.py
content: #!/usr/bin/env python3
# /// script
# requires-python = ">=3.10"
# dependencies = [
#     "requests>=2.31.0",
# ]
# ///
"""
V10.4 商业化图片生成器 — 意图识别 + 模板 + 结构化Prompt + 智能路由

作者: 圆规
GitHub: https://github.com/xyva-yuangui/beauty-image
License: MIT

升级要点 (vs V2):
  1. 精准意图识别: 30+场景自动匹配 (规则快速路径 + LLM深度解析)
  2. 结构化Prompt: 6层结构 (核心→主体→风格→光照→构图→技术)
  3. 30+场景模板: 名片/海报/头像/产品/水晶/毛绒/天气卡片/分镜...
  4. 15+风格词典: 赛博朋克/水墨/浮世绘/吉卜力/等距微缩...
  5. 智能引擎路由: 按场景+风格自动选wanx/seedream4/seedream5
  6. 自适应尺寸: 按场景类型自动推荐比例

用法:
  # 智能模式 (自动识别意图+模板+路由)
  uv run generate_image_v3.py --prompt "帮我做一张赛博朋克风格的个人名片"
  uv run generate_image_v3.py --prompt "画一只猫的水晶质感手办"
  uv run generate_image_v3.py --prompt "上海天气卡片"

  # 指定场景模板
  uv run generate_image_v3.py --prompt "猫" --scene 3d_crystal
  uv run generate_image_v3.py --prompt "日本武士" --scene art_ukiyoe_card --style 浮世绘

  # 指定字段 (JSON)
  uv run generate_image_v3.py --prompt "名片" --scene biz_card --fields '{"name":"圆规","title":"CEO","company":"XyvaClaw"}'

  # 兼容V2模式 (直接传prompt, 无模板)
  uv run generate_image_v3.py --prompt "一幅美丽的风景画" --raw

  # 列出可用模板/风格
  uv run generate_image_v3.py --list-scenes
  uv run generate_image_v3.py --list-styles
"""

import argparse
import json
import os
import sys
from pathlib import Path

# 添加当前目录到path以导入模块
sys.path.insert(0, str(Path(__file__).parent))

from image_prompt_templates import (
    build_prompt, list_scenes, list_styles, resolve_scene, resolve_style,
    QUALITY_NEGATIVE, QUALITY_SUFFIX, SCENE_TEMPLATES,
)
from image_intent_parser import parse_intent, generate_confirmation_message


# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# API Key 管理 (复用V2逻辑)
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

def get_wanx_key() -> str | None:
    """获取通义万相 API Key

    查找顺序:
      1. 环境变量 DASHSCOPE_API_KEY
      2. ~/.openclaw/openclaw.json → models.providers.wanxiang.apiKey
    
    获取地址: https://dashscope.console.aliyun.com/apiKey
    """
    key = os.environ.get("DASHSCOPE_API_KEY")
    if key:
        return key
    try:
        config_path = Path.home() / ".openclaw" / "openclaw.json"
        with open(config_path) as f:
            config = json.load(f)
        key = config.get("models", {}).get("providers", {}).get("wanxiang", {}).get("apiKey")
        if key and not key.startswith("sk-sp-"):
            return key
    except Exception:
        pass
    return None


def get_ark_key() -> str | None:
    """获取火山引擎 ARK API Key (Seedream 模型)

    查找顺序:
      1. 环境变量 ARK_API_KEY 或 VOLC_API_KEY
      2. ~/.openclaw/openclaw.json → models.providers.ark.apiKey

    获取地址: https://console.volcengine.com/ark/region:ark+cn-beijing/apiKey
    """
    key = os.environ.get("ARK_API_KEY") or os.environ.get("VOLC_API_KEY")
    if key:
        return key
    try:
        config_path = Path.home() / ".openclaw" / "openclaw.json"
        with open(config_path) as f:
            config = json.load(f)
        for provider_name in ["ark", "volcengine", "doubao"]:
            key = config.get("models", {}).get("providers", {}).get(provider_name, {}).get("apiKey")
            if key:
                return key
    except Exception:
        pass
    return None


def get_deepseek_key() -> str | None:
    """获取DeepSeek API Key (用于意图解析, 可选)

    查找顺序:
      1. 环境变量 DEEPSEEK_API_KEY
      2. ~/.openclaw/openclaw.json → models.providers.deepseek.apiKey

    获取地址: https://platform.deepseek.com/api_keys
    """
    key = os.environ.get("DEEPSEEK_API_KEY")
    if key:
        return key
    try:
        config_path = Path.home() / ".openclaw" / "openclaw.json"
        with open(config_path) as f:
            config = json.load(f)
        return config.get("models", {}).get("providers", {}).get("deepseek", {}).get("apiKey")
    except Exception:
        return None


# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# 尺寸预设
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

# Seedream 尺寸预设 (2K/3K 高分辨率)
SIZE_PRESETS = {
    "2K": "2048x2048", "2K_16:9": "2848x1600", "2K_9:16": "1600x2848",
    "2K_4:3": "2304x1728", "2K_3:4": "1728x2304",
    "2K_2:3": "1672x2512", "2K_3:2": "2512x1672",
    "2K_1:2": "1448x2896", "2K_2:1": "2896x1448",
    "2K_21:9": "3128x1344", "2K_9:21": "1344x3128",
    "3K": "3072x3072", "3K_16:9": "4096x2304", "3K_9:16": "2304x4096",
    "3K_4:3": "3456x2592", "3K_3:4": "2592x3456",
}

# wan2.6 尺寸限制: 总像素在 [1280*1280, 1440*1440] 之间, 宽高比 [1:4, 4:1]
# 支持 15 种比例 — 官方推荐5种 + 扩展10种
WANX_SIZE_MAP = {
    # 官方推荐尺寸
    "1:1":  "1280*1280",
    "3:4":  "1104*1472",
    "4:3":  "1472*1104",
    "9:16": "960*1696",
    "16:9": "1696*960",
    # 扩展尺寸 (经过像素总量验证)
    "2:3":  "1048*1568",
    "3:2":  "1568*1048",
    "1:2":  "904*1816",
    "2:1":  "1808*912",
    "9:21": "840*1960",
    "21:9": "1952*840",
    "1:3":  "744*2224",
    "3:1":  "2216*744",
    "1:4":  "640*2560",
    "4:1":  "2560*640",
}

# 用户友好别名 → 比例名
SIZE_ALIASES = {
    # 正方形
    "正方": "1:1", "square": "1:1", "头像": "1:1",
    # 横向
    "横版": "16:9", "风景": "16:9", "landscape": "16:9",
    "宽屏": "21:9", "ultrawide": "21:9", "电影": "21:9", "cinematic": "21:9",
    "横幅": "3:1", "banner": "3:1", "横幅海报": "3:1",
    "超宽": "4:1", "全景": "4:1", "panorama": "4:1",
    # 竖向
    "竖版": "9:16", "portrait": "9:16", "手机壁纸": "9:16",
    "竖屏": "9:21", "长图": "9:21",
    "海报": "2:3", "poster": "2:3",
    "长海报": "1:3", "长图海报": "1:3",
    "超长": "1:4", "滚动长图": "1:4",
    # 常见设备
    "小红书": "3:4", "xhs": "3:4",
    "A4": "3:4", "a4": "3:4",
    "PPT": "16:9", "ppt": "16:9", "幻灯片": "16:9",
    "名片": "16:9", "card": "16:9",
}

# Seedream/通用尺寸→wanx合法尺寸的自动降级映射
WANX_SIZE_FALLBACK = {
    "2K": "1280*1280", "2K_16:9": "1696*960", "2K_9:16": "960*1696",
    "2K_4:3": "1472*1104", "2K_3:4": "1104*1472",
    "2K_2:3": "1048*1568", "2K_3:2": "1568*1048",
    "2K_1:2": "904*1816", "2K_2:1": "1808*912",
    "2K_21:9": "1952*840", "2K_9:21": "840*1960",
    "3K": "1280*1280", "3K_16:9": "1696*960", "3K_9:16": "960*1696",
    "3K_4:3": "1472*1104", "3K_3:4": "1104*1472",
}


def resolve_size(size_input: str, provider: str) -> str:
    """解析尺寸参数

    支持多种格式:
      - 比例: "1:1", "16:9", "9:16", "2:3", "1:4" 等
      - 别名: "横版", "竖版", "海报", "全景", "小红书", "PPT" 等
      - 预设: "2K", "2K_16:9", "3K" 等 (seedream用)
      - 直接尺寸: "1280*1280", "1696x960" 等
    wanx自动缩放到合法像素范围, seedream直接使用高分辨率。
    """
    import math
    normalized = size_input.strip()

    # Step 1: 别名解析
    if normalized in SIZE_ALIASES:
        normalized = SIZE_ALIASES[normalized]

    if provider == "wanx":
        # Step 2a: 直接查wanx比例表
        if normalized in WANX_SIZE_MAP:
            return WANX_SIZE_MAP[normalized]
        # Step 2b: 查seedream降级表
        if normalized in WANX_SIZE_FALLBACK:
            return WANX_SIZE_FALLBACK[normalized]
        # Step 2c: 解析预设或直接尺寸, 自动缩放
        resolved = SIZE_PRESETS.get(normalized, normalized).replace("x", "*")
        try:
            parts = resolved.split("*")
            w, h = int(parts[0]), int(parts[1])
            total = w * h
            ratio = w / h
            min_px, max_px = 1280 * 1280, 1440 * 1440
            # 检查宽高比限制
            if ratio < 0.25 or ratio > 4.0:
                return "1280*1280"
            if min_px <= total <= max_px:
                return f"{w}*{h}"
            # 缩放到合法范围 (8像素对齐)
            target = min_px
            new_h = int(math.sqrt(target / ratio))
            new_h = (new_h // 8) * 8
            new_w = int(new_h * ratio)
            new_w = (new_w // 8) * 8
            if new_w * new_h < min_px:
                new_h += 8
            return f"{new_w}*{new_h}"
        except (ValueError, IndexError):
            return "1280*1280"
    else:
        # Seedream: 支持高分辨率
        if normalized in WANX_SIZE_MAP:
            # 比例名 → seedream用对应的2K尺寸
            ratio_to_2k = {
                "1:1": "2K", "16:9": "2K_16:9", "9:16": "2K_9:16",
                "4:3": "2K_4:3", "3:4": "2K_3:4",
                "2:3": "2K_2:3", "3:2": "2K_3:2",
                "1:2": "2K_1:2", "2:1": "2K_2:1",
                "21:9": "2K_21:9", "9:21": "2K_9:21",
            }
            preset_key = ratio_to_2k.get(normalized)
            if preset_key and preset_key in SIZE_PRESETS:
                return SIZE_PRESETS[preset_key].replace("*", "x")
        resolved = SIZE_PRESETS.get(normalized, normalized)
        return resolved.replace("*", "x")


# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# 引擎路由
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

ENGINE_MODEL_MAP = {
    "wanx": "wan2.6-t2i",         # 主力: 同步调用, 新协议
    "wanx_fallback": "wan2.5-t2i-preview",  # 备选: 异步调用, 旧协议
    "seedream4": "doubao-seedream-4-0-250828",
    "seedream5": "doubao-seedream-5-0-260128",
}

# wan2.6 尺寸要求: 总像素在[1280*1280, 1440*1440]之间, 宽高比[1:4, 4:1]
# wan2.5 同wan2.6 — 完整尺寸表见上方 WANX_SIZE_MAP (15种比例)
WAN26_SIZE_PRESETS = WANX_SIZE_MAP


def select_engine(recommended: str, fallback: str, force_engine: str | None = None) -> str:
    """选择最终引擎 (检查key可用性)"""
    if force_engine:
        return force_engine

    ark_ok = bool(get_ark_key())
    wanx_ok = bool(get_wanx_key())

    if recommended.startswith("seedream") and ark_ok:
        return recommended
    if recommended == "wanx" and wanx_ok:
        return recommended

    # 降级
    if fallback.startswith("seedream") and ark_ok:
        return fallback
    if fallback == "wanx" and wanx_ok:
        return fallback

    # 最终降级
    if wanx_ok:
        return "wanx"
    if ark_ok:
        return "seedream4"

    # 无任何可用key, 仍返回wanx让后续generate_wanx给出详细错误提示
    return "wanx"


# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# 图片生成 API 调用
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

def generate_wanx(prompt: str, negative: str, size: str, filename: str | None,
                  no_prompt_extend: bool, watermark: bool) -> dict:
    """通义万相图片生成 — wan2.6同步(主) + wan2.5异步(备)"""
    import requests

    [REDACTED_SECRET])
    if not [REDACTED_SECRET] {
            "success": False,
            "error": (
                "缺少通义万相 API Key (DASHSCOPE_API_KEY)。\n"
                "请通过以下方式之一配置:\n"
                "  1. 设置环境变量: export DASHSCOPE_API_KEY=your-key\n"
                "  2. 在 ~/.openclaw/openclaw.json 中配置: models.providers.wanxiang.apiKey\n"
                "获取地址: https://dashscope.console.aliyun.com/apiKey"
            ),
        }
    size_str = resolve_size(size, "wanx")

    # ── 优先: wan2.6-t2i 同步调用 (新协议, 一次请求直接返回图片) ──
    model_26 = ENGINE_MODEL_MAP["wanx"]
    print(f"🎨 [万相 {model_26}] 同步生成中...")
    print(f"  Prompt: {prompt[:100]}...")
    print(f"  Negative: {negative[:60]}...")
    print(f"  Size: {size_str}")

    payload_26 = {
        "model": model_26,
        "input": {
            "messages": [{"role": "user", "content": [{"text": prompt}]}]
        },
        "parameters": {
            "negative_prompt": negative,
            "prompt_extend": not no_prompt_extend,
            "watermark": watermark,
            "size": size_str,
            "n": 1,
        },
    }

    try:
        resp = requests.post(
            "https://dashscope.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation",
            headers={"Content-Type": "application/json", "Authorization": f"Bearer {api_key}"},
            json=payload_26, timeout=120,
        )

        if resp.status_code == 200:
            result = resp.json()
            choices = result.get("output", {}).get("choices", [])
            if choices:
                content = choices[0].get("message", {}).get("content", [])
                if content and content[0].get("image"):
                    return _finalize(content[0]["image"], filename, model_26, requests)

            return {"success": False, "error": f"wan2.6响应无图片: {result}"}

        # wan2.6失败, 尝试降级到wan2.5
        err_msg = ""
        try:
            err_msg = resp.json().get("message", resp.text[:200])
        except Exception:
            err_msg = resp.text[:200]
        print(f"  ⚠️ wan2.6失败 ({resp.status_code}: {err_msg}), 降级到wan2.5...")

    except Exception as e:
        print(f"  ⚠️ wan2.6异常 ({e}), 降级到wan2.5...")

    # ── 降级: wan2.5-t2i-preview 异步调用 (旧协议, 需轮询) ──
    return _generate_wanx_legacy(prompt, negative, size_str, filename, api_key, requests)


def _generate_wanx_legacy(prompt: str, negative: str, size_str: str,
                          filename: str | None, api_key: str, requests_mod) -> dict:
    """wan2.5-t2i-preview 异步调用 (旧协议fallback)"""
    import time

    model_25 = ENGINE_MODEL_MAP["wanx_fallback"]
    print(f"\n🎨 [万相 {model_25}] 异步生成中...")

    payload = {
        "model": model_25,
        "input": {"prompt": prompt, "negative_prompt": negative},
        "parameters": {"size": size_str, "n": 1},
    }

    resp = requests_mod.post(
        "https://dashscope.aliyuncs.com/api/v1/services/aigc/text2image/image-synthesis",
        headers={
            "Content-Type": "application/json",
            "Authorization": f"Bearer {api_key}",
            "X-DashScope-Async": "enable",
        },
        json=payload, timeout=30,
    )

    if resp.status_code != 200:
        try:
            return {"success": False, "error": f"wan2.5提交失败: {resp.json().get('message', '?')}"}
        except Exception:
            return {"success": False, "error": f"HTTP {resp.status_code}: {resp.text[:200]}"}

    task_id = resp.json().get("output", {}).get("task_id")
    if not task_id:
        return {"success": False, "error": f"未返回task_id"}

    print(f"  任务已提交: {task_id}")

    # 轮询 (最长120秒)
    for i in range(40):
        time.sleep(3)
        try:
            r = requests_mod.get(
                f"https://dashscope.aliyuncs.com/api/v1/tasks/{task_id}",
                headers={"Authorization": f"Bearer {api_key}"},
                timeout=15,
            )
            task_result = r.json()
        except Exception as e:
            print(f"  轮询异常: {e}")
            continue

        status = task_result.get("output", {}).get("task_status", "UNKNOWN")
        if i % 3 == 0:
            print(f"  等待中... ({(i+1)*3}s, 状态: {status})")

        if status == "SUCCEEDED":
            # 旧协议: output.results[0].url
            results = task_result.get("output", {}).get("results", [])
            if results and results[0].get("url"):
                return _finalize(results[0]["url"], filename, model_25, requests_mod)
            # 新协议兼容: output.choices[0].message.content[0].image
            choices = task_result.get("output", {}).get("choices", [])
            if choices:
                content = choices[0].get("message", {}).get("content", [])
                if content and content[0].get("image"):
                    return _finalize(content[0]["image"], filename, model_25, requests_mod)
            return {"success": False, "error": "任务成功但无图片URL"}

        elif status in ("FAILED", "UNKNOWN"):
            err_msg = task_result.get("output", {}).get("message", "未知错误")
            return {"success": False, "error": f"任务失败: {err_msg}"}

    return {"success": False, "error": "轮询超时 (120s)"}


def generate_seedream(prompt: str, model: str, size: str, filename: str | None,
                      watermark: bool, sequential: str, max_images: int,
                      optimize_prompt_mode: str) -> dict:
    """Seedream图片生成"""
    import requests

    [REDACTED_SECRET])
    if not [REDACTED_SECRET] {
            "success": False,
            "error": (
                "缺少火山引擎 ARK API Key。\n"
                "请通过以下方式之一配置:\n"
                "  1. 设置环境变量: export ARK_API_KEY=your-key\n"
                "  2. 在 ~/.openclaw/openclaw.json 中配置: models.providers.ark.apiKey\n"
                "获取地址: https://console.volcengine.com/ark/region:ark+cn-beijing/apiKey"
            ),
        }
    size_str = resolve_size(size, "seedream")

    payload = {
        "model": model,
        "prompt": prompt,
        "size": size_str,
        "watermark": watermark,
        "response_format": "url",
    }
    if sequential != "disabled":
        payload["sequential_image_generation"] = sequential
        if max_images > 1:
            payload["sequential_image_generation_options"] = {"max_images": max_images}
    if model in ("doubao-seedream-5-0-260128", "doubao-seedream-4.5", "doubao-seedream-4-0-250828"):
        payload["optimize_prompt_options"] = {"mode": optimize_prompt_mode}
    if model == "doubao-seedream-5-0-260128":
        payload["output_format"] = "png"

    model_short = model.replace("doubao-seedream-", "SD")
    print(f"🎨 [{model_short}] 生成中...")
    print(f"  Prompt: {prompt[:100]}...")
    print(f"  Size: {size_str}")

    resp = requests.post(
        "https://ark.cn-beijing.volces.com/api/v3/images/generations",
        headers={"Content-Type": "application/json", "Authorization": f"Bearer {api_key}"},
        json=payload, timeout=180,
    )

    if resp.status_code != 200:
        try:
            err = resp.json()
            return {"success": False, "error": f"HTTP {resp.status_code}: {err.get('error', {}).get('message', resp.text[:200])}"}
        except Exception:
            return {"success": False, "error": f"HTTP {resp.status_code}: {resp.text[:200]}"}

    result = resp.json()
    if result.get("error"):
        return {"success": False, "error": result["error"].get("message", str(result["error"]))}

    data = result.get("data", [])
    if not data:
        return {"success": False, "error": "无返回图片"}

    images = []
    for item in data:
        if item.get("url"):
            images.append({"url": item["url"], "size": item.get("size", size_str)})
        elif item.get("error"):
            images.append({"error": item["error"].get("message", "未知错误")})

    if not images:
        return {"success": False, "error": "未解析到图片"}

    first_url = images[0].get("url")
    if filename and first_url:
        return _finalize(first_url, filename, model_short, requests, extra_images=images[1:])

    return {
        "success": True, "provider": model,
        "images": images, "image_count": len(images),
    }


def _finalize(image_url: str, filename: str | None, provider: str,
              requests_mod, extra_images: list | None = None) -> dict:
    """下载+保存+输出"""
    result = {"success": True, "provider": provider, "image_url": image_url}
    print(f"\n✅ 图片生成成功!")
    print(f"Image URL: {image_url}")

    if filename:
        output_path = Path(filename)
        output_path.parent.mkdir(parents=True, exist_ok=True)
        img_resp = requests_mod.get(image_url, timeout=60)
        img_resp.raise_for_status()
        with open(output_path, "wb") as f:
            f.write(img_resp.content)
        full_path = output_path.resolve()
        result["local_path"] = str(full_path)
        print(f"Saved to: {full_path}")
        print(f"MEDIA: {full_path}")
    else:
        print(f"MEDIA_URL: {image_url}")

    if extra_images:
        result["extra_images"] = extra_images
        for img in extra_images:
            if img.get("url"):
                print(f"MEDIA_URL: {img['url']}")

    return result


# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# 主流程
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

def smart_generate(user_input: str, scene: str | None = None, style: str | None = None,
                   fields: dict | None = None, force_engine: str | None = None,
                   size: str | None = None, quality: str = "high",
                   filename: str | None = None, raw: bool = False,
                   no_prompt_extend: bool = False, watermark: bool = False,
                   sequential: str = "disabled", max_images: int = 1,
                   optimize_prompt: str = "standard", use_llm: bool = False) -> dict:
    """V3 智能图片生成主流程

    流程: 意图解析 → 模板匹配 → Prompt构建 → 引擎路由 → 生成
    """

    # ── Step 0: Raw模式 (兼容V2, 直接生成) ──
    if raw:
        has_chinese = any('\u4e00' <= c <= '\u9fff' for c in user_input)
        lang = "zh" if has_chinese else "en"
        suffix = QUALITY_SUFFIX.get(quality, QUALITY_SUFFIX["standard"])[lang]
        prompt = user_input
        if "8K" not in prompt and "高清" not in prompt:
            prompt += suffix
        negative = QUALITY_NEGATIVE.get(quality, QUALITY_NEGATIVE["standard"])
        engine = force_engine or ("seedream4" if get_ark_key() else "wanx")
        size = size or "2K"

        print(f"📝 [Raw模式] 直接生成")
        return _do_generate(prompt, negative, engine, size, filename,
                           no_prompt_extend, watermark, sequential, max_images, optimize_prompt)

    # ── Step 1: 意图解析 ──
    print(f"🔍 解析意图: \"{user_input[:60]}...\"")

    if scene:
        # 用户显式指定场景
        intent = {
            "scene_id": scene,
            "style": style,
            "subject": user_input,
            "fields": fields or {"subject": user_input},
            "missing_fields": [],
            "confidence": 1.0,
            "method": "explicit",
        }
    else:
        # 自动解析
        ds_key = get_deepseek_key() if use_llm else None
        if use_llm and not ds_key:
            print("⚠️ --use-llm 已启用但未找到 DEEPSEEK_API_KEY，将使用规则快速路径解析")
            print("   获取地址: https://platform.deepseek.com/api_keys")
        intent = parse_intent(user_input, use_llm=use_llm, [REDACTED_SECRET])
        if style:
            intent["style"] = style
        if fields:
            intent["fields"].update(fields)

    scene_id = intent.get("scene_id")
    style_name = intent.get("style")
    parsed_fields = intent.get("fields", {})

    # 确保subject存在
    if "subject" not in parsed_fields:
        parsed_fields["subject"] = intent.get("subject", user_input)

    print(f"  场景: {scene_id or '通用'} | 风格: {style_name or '默认'} | 方法: {intent.get('method', '?')}")
    print(f"  置信度: {intent.get('confidence', 0):.1%}")

    # ── Step 2: 构建Prompt ──
    build_result = build_prompt(scene_id, style_name, parsed_fields, quality)

    prompt = build_result["prompt"]
    negative = build_result["negative"]
    recommended_engine = build_result["engine"]
    fallback_engine = build_result["fallback_engine"]
    recommended_size = build_result["size"]
    scene_name = build_result["scene_name"]

    print(f"\n📋 模板: {scene_name}")
    print(f"  推荐引擎: {recommended_engine} | 备选: {fallback_engine}")
    print(f"  推荐尺寸: {recommended_size}")

    # ── Step 3: 引擎路由 ──
    engine = select_engine(recommended_engine, fallback_engine, force_engine)
    final_size = size or recommended_size

    print(f"  最终引擎: {engine} | 尺寸: {final_size}")

    # ── Step 4: 生成 ──
    return _do_generate(prompt, negative, engine, final_size, filename,
                       no_prompt_extend, watermark, sequential, max_images, optimize_prompt)


def _do_generate(prompt: str, negative: str, engine: str, size: str,
                 filename: str | None, no_prompt_extend: bool, watermark: bool,
                 sequential: str, max_images: int, optimize_prompt: str) -> dict:
    """执行实际的图片生成调用"""
    try:
        if engine == "wanx":
            return generate_wanx(prompt, negative, size, filename, no_prompt_extend, watermark)
        elif engine in ("seedream4", "seedream5"):
            model = ENGINE_MODEL_MAP[engine]
            return generate_seedream(prompt, model, size, filename, watermark,
                                    sequential, max_images, optimize_prompt)
        else:
            return {"success": False, "error": f"未知引擎: {engine}"}
    except Exception as e:
        return {"success": False, "error": str(e)}


# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# CLI
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

def main():
    parser = argparse.ArgumentParser(description="V10.4 商业化图片生成器")

    # 主参数
    parser.add_argument("--prompt", "-p", help="图片描述 / 用户自然语言输入")
    parser.add_argument("--scene", help="指定场景模板ID (如: biz_card, 3d_crystal)")
    parser.add_argument("--style", help="指定风格 (如: 赛博朋克, 水墨画, 吉卜力)")
    parser.add_argument("--fields", help="模板字段 (JSON格式, 如: '{\"name\":\"圆规\"}')")

    # 引擎/质量
    parser.add_argument("--engine", choices=["wanx", "seedream4", "seedream5"],
                        help="强制指定引擎 (默认: 模板推荐)")
    parser.add_argument("--size", "-s", help="尺寸 (如: 2K, 2K_16:9, 3K, wanx_1:1)")
    parser.add_argument("--quality", "-q", choices=["high", "standard"], default="high")

    # 模式
    parser.add_argument("--raw", action="store_true",
                        help="Raw模式: 跳过意图解析, 直接用prompt生成 (兼容V2)")
    parser.add_argument("--use-llm", action="store_true",
                        help="启用LLM意图解析 (低置信度时调用DeepSeek)")

    # 输出
    parser.add_argument("--filename", "-f", help="保存到文件")
    parser.add_argument("--no-prompt-extend", action="store_true")
    parser.add_argument("--watermark", action="store_true")

    # Seedream特有
    parser.add_argument("--sequential", choices=["auto", "disabled"], default="disabled")
    parser.add_argument("--max-images", type=int, default=1)
    parser.add_argument("--optimize-prompt", choices=["standard", "fast"], default="standard")

    # 列表
    parser.add_argument("--list-scenes", action="store_true", help="列出所有场景模板")
    parser.add_argument("--list-styles", action="store_true", help="列出所有风格")
    parser.add_argument("--list-providers", action="store_true", help="列出可用引擎")
    parser.add_argument("--list-sizes", action="store_true", help="列出所有支持的尺寸和比例")

    args = parser.parse_args()

    # ── 列表模式 ──
    if args.list_scenes:
        print("📋 可用场景模板:")
        scenes = list_scenes()
        categories = {}
        for s in scenes:
            categories.setdefault(s["category"], []).append(s)
        for cat, items in categories.items():
            print(f"\n  [{cat}]")
            for item in items:
                print(f"    {item['id']:24s} {item['name']:10s}  引擎={item['engine']:10s}  尺寸={item['size']}")
        return

    if args.list_styles:
        print("🎨 可用风格:")
        for style_name in list_styles():
            print(f"  - {style_name}")
        return

    if args.list_providers:
        wanx_ok = bool(get_wanx_key())
        ark_ok = bool(get_ark_key())
        ds_ok = bool(get_deepseek_key())
        print("可用图片生成引擎:")
        print(f"  {'✅' if wanx_ok else '❌'} wanx      — 通义万相 wan2.6-t2i (阿里云)")
        print(f"  {'✅' if ark_ok else '❌'} seedream4 — doubao-seedream-4.0 (火山引擎)")
        print(f"  {'✅' if ark_ok else '❌'} seedream5 — doubao-seedream-5-0-260128 (火山引擎, 最高画质)")
        print(f"\n可选增强:")
        print(f"  {'✅' if ds_ok else 'ℹ️'} DeepSeek  — LLM深度意图解析 (--use-llm, 可选)")
        if not wanx_ok:
            print(f"\n  💡 设置 DASHSCOPE_API_KEY 以启用万相引擎")
            print(f"     获取: https://dashscope.console.aliyun.com/apiKey")
        if not ark_ok:
            print(f"\n  💡 设置 ARK_API_KEY 以启用 Seedream 引擎")
            print(f"     获取: https://console.volcengine.com/ark/region:ark+cn-beijing/apiKey")
        if not wanx_ok and not ark_ok:
            print(f"\n  ⚠️ 至少需要配置一个 API Key 才能使用图片生成功能")
        print(f"\n  作者: 圆规 | https://github.com/xyva-yuangui/beauty-image")
        return

    if args.list_sizes:
        print("📐 支持的尺寸和比例:\n")
        print("  [wanx 万相] (总像素 1280*1280 ~ 1440*1440)")
        for ratio, size in WANX_SIZE_MAP.items():
            w, h = size.split("*")
            orient = "■" if w == h else ("▬" if int(w) > int(h) else "▮")
            print(f"    {orient} {ratio:>5s}  →  {size}")
        print(f"\n  [seedream] (2K/3K 高分辨率)")
        for name, size in SIZE_PRESETS.items():
            print(f"    {name:>10s}  →  {size}")
        print(f"\n  [中文别名] (自动映射到比例)")
        alias_groups = {}
        for alias, ratio in SIZE_ALIASES.items():
            alias_groups.setdefault(ratio, []).append(alias)
        for ratio, aliases in alias_groups.items():
            print(f"    {ratio:>5s}  ←  {', '.join(aliases)}")
        print(f"\n  用法: --size 16:9 / --size 海报 / --size 小红书 / --size 全景 / --size 1280*720")
        return

    # ── 生成模式 ──
    if not args.prompt:
        parser.error("--prompt is required for image generation")

    fields = {}
    if args.fields:
        try:
            fields = json.loads(args.fields)
        except json.JSONDecodeError:
            print(f"❌ --fields 必须是合法的JSON格式", file=sys.stderr)
            sys.exit(1)

    result = smart_generate(
        user_input=args.prompt,
        scene=args.scene,
        style=args.style,
        fields=fields,
        force_engine=args.engine,
        size=args.size,
        quality=args.quality,
        filename=args.filename,
        raw=args.raw,
        no_prompt_extend=args.no_prompt_extend,
        watermark=args.watermark,
        sequential=args.sequential,
        max_images=args.max_images,
        optimize_prompt=args.optimize_prompt,
        use_llm=args.use_llm,
    )

    if not result.get("success"):
        print(f"\n❌ 生成失败: {result.get('error', '未知错误')}", file=sys.stderr)
        sys.exit(1)

    print(f"\nRESULT_JSON: {json.dumps(result, ensure_ascii=False)}")


if __name__ == "__main__":
    main()


path: scripts/image_intent_parser.py
content: #!/usr/bin/env python3
"""
V10.4 图片生成意图解析器 — 从用户自然语言提取结构化图片生成意图

作者: 圆规
GitHub: https://github.com/xyva-yuangui/beauty-image
License: MIT

支持两种模式:
  1. 规则快速路径: 关键词匹配 (<1ms)
  2. LLM深度解析: DeepSeek结构化分析 (可选, 需DEEPSEEK_API_KEY)
"""

import json
import re
from typing import Optional

from image_prompt_templates import SCENE_ALIASES, STYLE_ALIASES, SCENE_TEMPLATES, STYLE_DICT


# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# 规则快速路径 (Rule-based Fast Path)
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

# 场景关键词 → 场景ID (优先级从高到低)
_SCENE_RULES = [
    # 商务
    (r"(名片|business\s*card)", "biz_card"),
    (r"(海报|poster|宣传图|宣传画)", "biz_poster"),
    (r"(产品图|产品照|产品摄影|product\s*photo)", "biz_product"),
    # 社交
    (r"(头像|avatar|个人形象|IP形象|ip形象)", "social_avatar"),
    (r"(表情包|emoji|sticker|表情)", "social_emoji"),
    (r"(金句|语录|quote|名言).{0,5}(卡片|图|card)?", "social_card"),
    # 3D/材质
    (r"(水晶|crystal|玻璃|透明).{0,5}(质感|风格|效果|材质)?", "3d_crystal"),
    (r"(毛绒|plush|蓬松|绒毛).{0,5}(玩具|风格|效果|版)?", "3d_plush"),
    (r"(充气|inflatable|气球).{0,5}(玩具|风格|效果|版)?", "3d_inflatable"),
    (r"(微缩|miniature|等距|isometric|小场景|小房间|diorama)", "3d_miniature"),
    # 实用
    (r"(天气|weather).{0,5}(卡片|图|card)?", "util_weather"),
    (r"(冰箱贴|fridge\s*magnet|磁贴)", "util_fridge_magnet"),
    (r"(美食地图|food\s*map|食物地图)", "util_food_map"),
    (r"(分镜|storyboard|镜头脚本)", "util_storyboard"),
    # 设计
    (r"(PPT|ppt|幻灯片|slide)", "design_ppt"),
    (r"(logo|LOGO|标志|商标|徽标)", "design_logo"),
    (r"(包装|packaging|盒子设计)", "design_packaging"),
    # 人物
    (r"(肖像|portrait|写真|证件照)", "person_portrait"),
    (r"(手办|figure|模型|action\s*figure)", "person_action_figure"),
    # 场景
    (r"(黑板|chalkboard|粉笔画|粉笔)", "scene_chalkboard"),
    # 电商
    (r"(电商|主图|白底图|产品主图|淘宝|天猫).{0,5}(图|照)?", "ecom_main"),
    (r"(横幅|banner|促销图|活动图)", "ecom_banner"),
    # 内容
    (r"(小红书|xhs|种草).{0,5}(配图|图片|图)?", "content_xhs"),
    (r"(头图|封面图|blog|公众号|文章).{0,5}(配图|头图|封面)?", "content_blog"),
    # 特色
    (r"(浮世绘|ukiyo).{0,5}(闪卡|卡|卡牌|card)?", "art_ukiyoe_card"),
]

# 风格关键词 → 风格名称
_STYLE_RULES = [
    (r"赛博朋克|cyberpunk|科幻未来", "赛博朋克"),
    (r"浮世绘|ukiyo|日式传统", "浮世绘"),
    (r"水墨|国画|中国画|ink\s*wash", "水墨画"),
    (r"吉卜力|ghibli|宫崎骏|gibli", "吉卜力"),
    (r"像素|pixel|8.?bit|16.?bit|复古游戏", "像素风"),
    (r"油画|古典|文艺复兴|伦勃朗", "油画"),
    (r"漫画|manga|anime|动漫", "漫画"),
    (r"波普|pop\s*art|安迪沃霍尔", "波普艺术"),
    (r"水晶|crystal|玻璃质感|透明质感", "水晶"),
    (r"毛绒|plush|蓬松|绒毛", "毛绒"),
    (r"充气|inflatable|气球", "充气"),
    (r"乐高|lego|积木", "乐高"),
    (r"黏土|clay|橡皮泥|定格动画", "黏土"),
    (r"电影感|cinematic|电影画面", "电影感"),
    (r"产品摄影|产品照|白底|棚拍", "产品摄影"),
    (r"街拍|street|抓拍", "街拍"),
    (r"胶片|film|kodak|复古", "胶片"),
    (r"等距|isometric|微缩|3d微缩", "等距微缩"),
    (r"3d渲染|c4d|blender|三维", "3D渲染"),
    (r"清明上河图", "清明上河图"),
    (r"极简|minimalist|简约", None),  # 通用风格, 不映射到特定词典
    (r"ins风|instagram|小清新", None),
]

# 预编译正则
_COMPILED_SCENE_RULES = [(re.compile(p, re.IGNORECASE), sid) for p, sid in _SCENE_RULES]
_COMPILED_STYLE_RULES = [(re.compile(p, re.IGNORECASE), sname) for p, sname in _STYLE_RULES]


def parse_intent_fast(user_input: str) -> dict:
    """规则快速路径: 从用户输入提取场景和风格 (<1ms)

    返回:
        {
            "scene_id": str | None,
            "style": str | None,
            "subject": str,        # 提取的主体描述
            "fields": dict,        # 可填充的字段
            "missing_fields": list, # 还需要确认的必填字段
            "confidence": float,   # 0-1
        }
    """
    text = user_input.strip()

    # 场景匹配
    scene_id = None
    scene_confidence = 0.0
    for pattern, sid in _COMPILED_SCENE_RULES:
        if pattern.search(text):
            scene_id = sid
            scene_confidence = 0.8
            break

    # 风格匹配
    style_name = None
    for pattern, sname in _COMPILED_STYLE_RULES:
        if pattern.search(text):
            style_name = sname
            break

    # 提取主体 (去掉场景关键词和风格关键词后的核心描述)
    subject = text
    # 去掉常见的动词前缀
    subject = re.sub(r'^(帮我|请|给我|我要|我想|来一张?|做一张?|画一张?|生成一张?|创建一张?)', '', subject)
    # 去掉尾部动词
    subject = re.sub(r'(的图片?|的图|的照片|的设计)$', '', subject)
    subject = subject.strip()

    # 构建字段
    fields = {"subject": subject}

    # 特定场景的字段提取
    if scene_id == "biz_card":
        # 尝试从输入中提取名字
        name_match = re.search(r'(姓名|名字|叫|我是)[：:\s]*([^\s,，。]+)', text)
        if name_match:
            fields["name"] = name_match.group(2)
    elif scene_id == "util_weather":
        city_match = re.search(r'([\u4e00-\u9fff]{2,6})(的?天气|weather)', text)
        if city_match:
            fields["city"] = city_match.group(1)
    elif scene_id == "social_card":
        # 提取引号中的金句
        quote_match = re.search(r'[""「」『』](.*?)[""」」』』]', text)
        if quote_match:
            fields["text"] = quote_match.group(1)

    # 计算缺失字段
    missing_fields = []
    if scene_id and scene_id in SCENE_TEMPLATES:
        template = SCENE_TEMPLATES[scene_id]
        for field in template.get("required_fields", []):
            if field not in fields or not fields[field]:
                missing_fields.append(field)

    confidence = scene_confidence
    if style_name:
        confidence = min(confidence + 0.1, 1.0)
    if not scene_id and not style_name:
        confidence = 0.3  # 低置信, 可能需要LLM解析

    return {
        "scene_id": scene_id,
        "style": style_name,
        "subject": subject,
        "fields": fields,
        "missing_fields": missing_fields,
        "confidence": confidence,
    }


# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# LLM深度解析 (DeepSeek)
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

INTENT_PARSE_SYSTEM = """你是图片生成意图解析专家。分析用户的图片需求,输出JSON。

可用场景ID:
biz_card(名片), biz_poster(海报), biz_product(产品照), social_avatar(头像),
social_emoji(表情包), social_card(金句卡片), art_general(艺术创作),
3d_crystal(水晶质感), 3d_plush(毛绒), 3d_inflatable(充气), 3d_miniature(微缩场景),
util_weather(天气卡片), util_fridge_magnet(冰箱贴), util_food_map(美食地图),
util_storyboard(分镜), design_ppt(PPT), design_logo(Logo), design_packaging(包装),
person_portrait(肖像), person_action_figure(手办), scene_chalkboard(黑板粉笔画),
ecom_main(电商主图), ecom_banner(横幅), content_xhs(小红书配图),
content_blog(头图), art_ukiyoe_card(浮世绘闪卡)

可用风格: 赛博朋克, 浮世绘, 水墨画, 吉卜力, 像素风, 油画, 漫画, 波普艺术,
水晶, 毛绒, 充气, 乐高, 黏土, 电影感, 产品摄影, 街拍, 胶片, 等距微缩, 3D渲染, 清明上河图"""

INTENT_PARSE_PROMPT = """分析用户的图片生成请求,提取以下信息(仅输出JSON,不要其他文字):
{{
  "scene_id": "场景ID(从可用列表选, 无匹配则null)",
  "style": "风格名称(从可用列表选, 无匹配则null)",
  "subject": "主体描述(用户想生成什么)",
  "fields": {{
    "name": "名字(如有)",
    "title": "职称(如有)",
    "company": "公司(如有)",
    "city": "城市(如有)",
    "text": "需要渲染的文字(如有)",
    "color_scheme": "配色偏好(如有)",
    "mood": "情绪基调(如有)"
  }},
  "missing_info": ["还需要向用户确认的信息"],
  "recommended_size": "推荐尺寸比例(1:1/16:9/9:16/3:4/4:3)"
}}

用户输入: {user_input}"""


def parse_intent_llm(user_input: str, api_key: str, base_url: str = "https://api.deepseek.com/v1") -> dict:
    """LLM深度解析: 使用DeepSeek分析用户意图

    需要 requests 库和 DeepSeek API Key
    """
    import requests

    prompt = INTENT_PARSE_PROMPT.format(user_input=user_input)
    body = {
        "model": "deepseek-chat",
        "messages": [
            {"role": "system", "content": INTENT_PARSE_SYSTEM},
            {"role": "user", "content": prompt},
        ],
        "temperature": 0.1,
        "max_tokens": 500,
        "response_format": {"type": "json_object"},
    }

    try:
        resp = requests.post(
            f"{base_url}/chat/completions",
            headers={"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"},
            json=body,
            timeout=(3, 10),
        )
        if resp.status_code != 200:
            return {"error": f"HTTP {resp.status_code}", "fallback": True}

        content = resp.json()["choices"][0]["message"]["content"]
        parsed = json.loads(content)

        # 清理空值
        fields = {k: v for k, v in parsed.get("fields", {}).items() if v}

        return {
            "scene_id": parsed.get("scene_id"),
            "style": parsed.get("style"),
            "subject": parsed.get("subject", user_input),
            "fields": fields,
            "missing_fields": parsed.get("missing_info", []),
            "confidence": 0.9,
            "recommended_size": parsed.get("recommended_size"),
        }
    except Exception as e:
        return {"error": str(e), "fallback": True}


# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# 统一入口
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

def parse_intent(user_input: str, use_llm: bool = False,
                 api_key: str | None = None, base_url: str | None = None) -> dict:
    """统一意图解析入口

    策略:
      1. 先用规则快速路径
      2. 如果置信度低(<0.5) 且 use_llm=True, 调用LLM深度解析
      3. 合并结果

    返回:
        {
            "scene_id": str | None,
            "style": str | None,
            "subject": str,
            "fields": dict,
            "missing_fields": list,
            "confidence": float,
            "method": "rule" | "llm",
        }
    """
    # Step 1: 规则快速路径
    result = parse_intent_fast(user_input)
    result["method"] = "rule"

    # Step 2: 低置信时尝试LLM
    if result["confidence"] < 0.5 and use_llm and [REDACTED_SECRET] = parse_intent_llm(user_input, api_key, base_url or "https://api.deepseek.com/v1")
        if not llm_result.get("error"):
            # LLM结果覆盖规则结果
            result.update(llm_result)
            result["method"] = "llm"

    return result


# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# 用户交互消息生成
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

FIELD_QUESTIONS = {
    "name": "请提供姓名",
    "title": "请提供职称/头衔",
    "company": "请提供公司名称",
    "contact": "请提供联系方式 (电话/邮箱/网站)",
    "city": "请提供城市名称",
    "text": "请提供需要显示的文字内容",
    "subject": "请描述您想要生成的图片内容",
    "room_description": "请描述房间的主题和布置",
    "region": "请提供地区名称 (国家/省份)",
    "brand_name": "请提供品牌名称",
    "title": "请提供作品标题",
}


def generate_confirmation_message(intent: dict) -> str:
    """根据解析结果生成用户确认消息"""
    parts = []

    scene_id = intent.get("scene_id")
    style = intent.get("style")
    subject = intent.get("subject", "")

    if scene_id and scene_id in SCENE_TEMPLATES:
        scene = SCENE_TEMPLATES[scene_id]
        parts.append(f"识别到您想要生成: **{scene['name']}**")
    else:
        parts.append(f"识别到您想要生成图片")

    if style:
        parts.append(f"风格: **{style}**")

    if subject:
        parts.append(f"主题: {subject}")

    # 缺失字段
    missing = intent.get("missing_fields", [])
    if missing:
        parts.append("\n需要您提供以下信息:")
        for field in missing:
            question = FIELD_QUESTIONS.get(field, f"请提供{field}")
            parts.append(f"  - {question}")

    return "\n".join(parts)


path: scripts/image_prompt_templates.py
content: #!/usr/bin/env python3
"""
V10.5 图片生成模板系统 — 场景模板 + 风格词典 + 结构化Prompt Builder + 智能美化器

作者: 圆规
GitHub: https://github.com/xyva-yuangui/beauty-image
License: MIT

基于 PicoTrex/Awesome-Nano-Banana-images 110+模板 + YouMind 10000+提示词库研究
V10.5 升级: 40+场景 / 30+风格 / 智能Prompt美化器(啊哈时刻)
"""

# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# 风格词典 (Style Dictionary)
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

STYLE_DICT = {
    # ── 艺术风格 ──
    "赛博朋克": {
        "keywords": "赛博朋克风格, 霓虹灯光, 深色背景, 蓝紫粉渐变, 全息投影, 高科技未来感",
        "lighting": "霓虹灯混合光, 彩色边缘光, 逆光剪影",
        "negative": "白天, 明亮, 自然光, 田园, 温暖色调",
    },
    "浮世绘": {
        "keywords": "日式浮世绘风格, 粗细变化的墨笔轮廓线, 传统木版画配色, 戏剧性动态构图",
        "lighting": "平面化光照, 无阴影, 传统绘画光",
        "negative": "写实, 3D渲染, 照片, 现代, 数码感",
    },
    "水墨画": {
        "keywords": "中国传统水墨画风格, 留白, 墨色浓淡变化, 宣纸质感, 写意笔法",
        "lighting": "散射柔光, 自然天光, 无强烈阴影",
        "negative": "色彩饱和, 数码感, 3D, 写实, 西方油画",
    },
    "吉卜力": {
        "keywords": "吉卜力动画风格, 手绘质感, 温暖色调, 自然光, 细腻的背景细节, 宫崎骏美学",
        "lighting": "柔和的自然日光, 黄金时段, 温暖侧光",
        "negative": "暗色, 恐怖, 写实, 3D, 赛博朋克",
    },
    "像素风": {
        "keywords": "像素艺术风格, 8-bit/16-bit, 有限调色板, 清晰像素块, 复古游戏美学",
        "lighting": "平面光, 无柔和渐变",
        "negative": "写实, 高清, 柔和, 3D, 照片级",
    },
    "油画": {
        "keywords": "古典油画风格, 厚重的笔触纹理, 丰富的色彩层次, 画布质感, 文艺复兴构图",
        "lighting": "戏剧性的伦勃朗光, 明暗对比, 暖色调主光",
        "negative": "平面, 数码, 简笔画, 卡通, 现代",
    },
    "漫画": {
        "keywords": "日本漫画风格, 清晰的线条, 大眼睛, 动态表现, 网点/速度线效果",
        "lighting": "平面阴影, 明确的光影分区, 高对比",
        "negative": "写实, 油画, 水彩, 照片, 模糊",
    },
    "清明上河图": {
        "keywords": "传统中国水墨彩绘手卷, 古旧绢本质感, 张择端散点透视, 界画建筑绘画技法",
        "lighting": "柔和复古大地色调, 传统绘画均匀光",
        "negative": "现代, 写实照片, 3D, 鲜艳色彩, 数码",
    },
    "波普艺术": {
        "keywords": "波普艺术风格, 大胆鲜艳色彩, 网点印刷效果, 安迪沃霍尔, 高对比重复图案",
        "lighting": "均匀平面光, 无阴影, 高饱和",
        "negative": "暗色, 柔和, 自然, 写实, 水墨",
    },

    # ── 材质风格 ──
    "水晶": {
        "keywords": "透明玻璃或水晶材质, 照片级真实, 高抛光, 折射效果, 圆润倒角, 光滑曲面",
        "lighting": "明亮干净的专业棚拍光, 锐利高光, 微妙折射, 光线穿透产生折射弯曲",
        "negative": "哑光, 粗糙, 木质, 金属, 暗色",
    },
    "毛绒": {
        "keywords": "柔软蓬松的毛绒质感, 超逼真毛发纹理, 柔和阴影, 漂浮于浅灰色背景",
        "lighting": "摄影棚灯光, 柔和散射, 温暖",
        "negative": "光滑, 金属, 坚硬, 锋利, 冰冷",
    },
    "充气": {
        "keywords": "充气玩具材质, 气球般圆润, 柔软光泽, 鲜艳色彩, 可爱",
        "lighting": "柔和均匀光, 微微反光, 童趣",
        "negative": "写实, 暗色, 恐怖, 金属, 锋利",
    },
    "乐高": {
        "keywords": "乐高积木风格, 方块化造型, 鲜艳塑料色彩, 乐高小人, 微缩场景",
        "lighting": "明亮均匀, 棚拍, 柔和阴影",
        "negative": "写实, 有机形态, 柔软, 自然",
    },
    "黏土": {
        "keywords": "黏土/橡皮泥风格, 手工捏制质感, 柔和的指纹痕迹, 饱和色彩, 定格动画美学",
        "lighting": "柔和的顶光, 微妙阴影, 温暖",
        "negative": "写实, 金属, 光滑, 数码, 锋利",
    },

    # ── 摄影风格 ──
    "电影感": {
        "keywords": "电影画面质感, 2.39:1宽银幕, 胶片颗粒, 电影调色, 浅景深",
        "lighting": "戏剧性侧光, 明暗对比, 伦勃朗光",
        "negative": "平面, 明亮均匀, 过曝, 数码感",
    },
    "产品摄影": {
        "keywords": "专业产品摄影, 干净白底/渐变背景, 精致布光, 商业级后期",
        "lighting": "三点布光, 柔和主光, 轮廓光, 反射板补光",
        "negative": "杂乱背景, 自然光, 噪点, 暗角, 低画质",
    },
    "街拍": {
        "keywords": "街头摄影风格, 自然随性, 城市背景, 浅景深, 35mm镜头感",
        "lighting": "自然光, 侧逆光, 黄金时段",
        "negative": "棚拍, 白底, 摆拍, 过度修饰",
    },
    "胶片": {
        "keywords": "复古胶片摄影, Kodak Portra色调, 柔和颗粒, 微微褪色, 温暖偏色",
        "lighting": "自然光, 柔和, 温暖色温",
        "negative": "数码感, 过于锐利, 高饱和, HDR, 冷色",
    },

    # ── 3D风格 ──
    "等距微缩": {
        "keywords": "等距3D微缩场景, 45°俯视视角, 精细建模, PBR材质, 卡通渲染",
        "lighting": "柔和全局光照, 环境光遮蔽, 微妙阴影",
        "negative": "平面, 写实, 照片, 第一人称, 俯拍",
    },
    "3D渲染": {
        "keywords": "高品质3D渲染, C4D/Blender风格, 光滑表面, 柔和渐变, 现代设计感",
        "lighting": "HDRI环境光, 柔和反射, 玻璃/金属质感",
        "negative": "平面, 粗糙, 噪点, 低多边形",
    },

    # ── V10.5 新增风格 (YouMind研究) ──
    "水彩": {
        "keywords": "水彩画风格, 透明色彩层叠, 湿润渗透效果, 画纸纹理, 自然水痕边缘, 柔和色彩过渡",
        "lighting": "柔和自然光, 透光感, 轻盈",
        "negative": "厚重, 油画, 数码, 锐利边缘, 高饱和",
    },
    "素描线稿": {
        "keywords": "铅笔素描风格, 精细线条, 交叉阴影线, 纸张纹理, 黑白灰层次, 手绘质感",
        "lighting": "单向定向光, 明确光影分区, 铅笔交叉线表达阴影",
        "negative": "彩色, 数码, 3D, 照片, 模糊",
    },
    "Q版萌系": {
        "keywords": "Q版风格, 超大头身比(2-3头身), 圆润可爱, 大眼睛, 简化肢体, 鲜艳配色, 萌系设计",
        "lighting": "明亮均匀, 无重阴影, 轻快",
        "negative": "写实, 恐怖, 暗色, 复杂, 细长比例",
    },
    "复古怀旧": {
        "keywords": "复古怀旧风格, 褪色胶片色调, 暖黄偏色, 老照片颗粒, 柔和边缘暗角, 60-80年代美学",
        "lighting": "温暖日光, 柔和侧光, 略微过曝",
        "negative": "数码感, 冷色, 高饱和, 现代, 锐利",
    },
    "极简主义": {
        "keywords": "极简主义设计, 大量留白, 几何形状, 有限色彩(2-3色), 克制构图, 高级感",
        "lighting": "均匀柔光, 无戏剧性阴影, 干净",
        "negative": "杂乱, 过度装饰, 多色, 复杂纹理, 繁琐",
    },
    "电影剧照": {
        "keywords": "电影剧照级画面, 宽银幕2.39:1, 电影调色LUT, 浅景深散焦光斑, 胶片颗粒, 戏剧张力",
        "lighting": "伦勃朗光/蝴蝶光, 面部高对比, 背光轮廓, 实际光源(practicals)",
        "negative": "平面光, 明亮均匀, 卡通, 过曝, 手机拍摄感",
    },
    "动漫插画": {
        "keywords": "日系动漫插画, 精致上色, 明亮色彩, 清晰线稿, 光影细腻, 背景氛围感强",
        "lighting": "逆光轮廓, 柔和面部光, 环境色反射, 丁达尔光线",
        "negative": "写实, 油画, 粗糙, 低画质, 变形",
    },
    "水墨插画": {
        "keywords": "现代水墨插画风格, 传统水墨与现代设计融合, 大胆留白, 笔触飞白, 点缀金色或朱红",
        "lighting": "散射天光, 空灵, 意境深远",
        "negative": "西方油画, 3D, 数码感, 高饱和, 杂乱",
    },
    "液态玻璃": {
        "keywords": "Apple液态玻璃风格, 85-90%透明度, 极细边框, 微妙投影, 浮动深度, 毛玻璃模糊背景, 现代UI美学",
        "lighting": "柔和环境光, 玻璃折射高光, 微妙焦散, 背景色反射",
        "negative": "不透明, 粗边框, 老旧, 粗糙, 暗色",
    },
    "全息镭射": {
        "keywords": "全息镭射效果, 彩虹色光谱, 金属材质底色, 光线角度变化产生色彩变幻, 高级感闪卡质感",
        "lighting": "多角度点光源, 彩虹反射, 高亮镜面",
        "negative": "哑光, 暗色, 自然, 朴素, 纸质",
    },
}

# 风格别名映射 (用户可能用的不同叫法)
STYLE_ALIASES = {
    "cyberpunk": "赛博朋克", "赛博": "赛博朋克", "科幻": "赛博朋克",
    "ukiyo-e": "浮世绘", "日式": "浮世绘",
    "ink": "水墨画", "国画": "水墨画", "中国画": "水墨画", "水墨": "水墨画",
    "ghibli": "吉卜力", "宫崎骏": "吉卜力", "动画": "吉卜力",
    "pixel": "像素风", "8bit": "像素风", "复古游戏": "像素风",
    "crystal": "水晶", "玻璃": "水晶", "透明": "水晶",
    "plush": "毛绒", "蓬松": "毛绒", "绒毛": "毛绒",
    "inflatable": "充气", "气球": "充气",
    "lego": "乐高", "积木": "乐高",
    "clay": "黏土", "橡皮泥": "黏土", "定格动画": "黏土",
    "cinematic": "电影感", "电影": "电影感",
    "product": "产品摄影", "产品": "产品摄影", "白底": "产品摄影",
    "street": "街拍", "抓拍": "街拍",
    "film": "胶片", "复古": "胶片",
    "isometric": "等距微缩", "微缩": "等距微缩", "小场景": "等距微缩",
    "3d": "3D渲染", "c4d": "3D渲染", "blender": "3D渲染",
    "油画": "油画", "古典": "油画",
    "manga": "漫画", "动漫": "漫画", "anime": "漫画",
    "pop art": "波普艺术", "波普": "波普艺术",
    "清明上河图": "清明上河图",
    "watercolor": "水彩", "水彩画": "水彩",
    "sketch": "素描线稿", "素描": "素描线稿", "线稿": "素描线稿", "pencil": "素描线稿",
    "chibi": "Q版萌系", "q版": "Q版萌系", "萌": "Q版萌系", "可爱": "Q版萌系",
    "retro": "复古怀旧", "vintage": "复古怀旧", "怀旧": "复古怀旧", "旧照片": "复古怀旧",
    "minimal": "极简主义", "极简": "极简主义", "简约": "极简主义",
    "film still": "电影剧照", "剧照": "电影剧照", "电影截图": "电影剧照",
    "anime": "动漫插画", "动漫": "动漫插画", "二次元": "动漫插画",
    "ink illustration": "水墨插画", "现代水墨": "水墨插画",
    "glass": "液态玻璃", "glassmorphism": "液态玻璃", "毛玻璃": "液态玻璃",
    "holographic": "全息镭射", "镭射": "全息镭射", "闪卡": "全息镭射", "彩虹": "全息镭射",
}


def resolve_style(style_name: str) -> dict | None:
    """解析风格名称, 返回风格词典条目"""
    if not style_name:
        return None
    name = style_name.strip().lower()
    # 直接匹配
    for key, val in STYLE_DICT.items():
        if key.lower() == name:
            return val
    # 别名匹配
    canonical = STYLE_ALIASES.get(name)
    if canonical and canonical in STYLE_DICT:
        return STYLE_DICT[canonical]
    # 模糊匹配 (包含关键词)
    for key, val in STYLE_DICT.items():
        if name in key.lower() or key.lower() in name:
            return val
    return None


# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# 场景模板 (Scene Templates)
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

SCENE_TEMPLATES = {
    # ── 商务 ──
    "biz_card": {
        "name": "名片设计",
        "category": "商务",
        "template": (
            "一张逼真的{style_desc}名片照片：左手拿着一张横向的亚克力无边框名片，"
            "尺寸与标准名片相同（3.5英寸×2英寸），几乎占据了整个画面。"
            "名片采用简洁的个人名片布局，没有任何横幅或背景图片。"
            "{style_detail}"
            "最终的名片应布局均衡、线条简洁、视觉层次清晰，适合专业印刷。"
            "姓名：{name} 职称：{title} 公司名称：{company}"
            "联系方式：{contact}"
        ),
        "required_fields": ["name"],
        "optional_fields": ["title", "company", "contact", "style"],
        "defaults": {"style": "赛博朋克", "title": "创始人", "company": "", "contact": ""},
        "engine": "seedream5", "fallback_engine": "seedream4",
        "size": "2K_16:9",
        "style_variants": {
            "赛博朋克": "它拥有光滑圆润的边缘，散发出柔和的霓虹光芒，并带有蓝色、粉色和紫色的渐变效果。背景采用深色虚化处理，以突出发光的边缘，营造出高科技的全息氛围。",
            "极简": "采用纯白色底色, 黑色文字, 极简主义设计, 大量留白, 仅保留必要信息。背景为干净的浅灰色。",
            "金属": "采用拉丝金属质感, 银色或深灰色底色, 压印凸起的文字, 边缘有精致的金色或银色线条。背景为暗色大理石桌面。",
        },
    },
    "biz_poster": {
        "name": "商业海报",
        "category": "商务",
        "template": (
            "一张专业的商业宣传海报设计，{style_desc}风格。"
            "主题：{subject}。"
            "海报采用{layout}布局，视觉层次清晰。"
            "主色调为{color_scheme}，搭配{mood}的整体氛围。"
            "{text_element}"
            "画面精致, 适合印刷和数字展示。"
        ),
        "required_fields": ["subject"],
        "optional_fields": ["style", "layout", "color_scheme", "mood", "text_element"],
        "defaults": {"style": "现代", "layout": "上图下文", "color_scheme": "品牌色", "mood": "专业", "text_element": ""},
        "engine": "seedream5", "fallback_engine": "seedream4",
        "size": "2K_9:16",
    },
    "biz_product": {
        "name": "产品摄影",
        "category": "商务",
        "template": (
            "一张专业级产品摄影照片，主体是{subject}。"
            "干净的{background}背景，产品位于画面中央。"
            "三点布光：柔和主光从左上方照射，轮廓光勾勒产品边缘，"
            "反射板从右下方补光，消除死角阴影。"
            "产品表面纹理清晰可见，材质质感真实。"
            "浅景深突出产品，背景柔和虚化。"
            "商业级后期调色，色彩准确还原。"
        ),
        "required_fields": ["subject"],
        "optional_fields": ["background"],
        "defaults": {"background": "纯白渐变"},
        "engine": "seedream4", "fallback_engine": "wanx",
        "size": "2K",
    },

    # ── 社交 ──
    "social_avatar": {
        "name": "头像/IP形象",
        "category": "社交",
        "template": (
            "一个精致的个人头像/IP形象设计，{style_desc}风格。"
            "人物描述：{subject}。"
            "表情{expression}，姿态自然。"
            "背景：{background}。"
            "整体风格统一，色彩和谐，适合社交媒体头像使用。"
            "高清细腻，细节丰富。"
        ),
        "required_fields": ["subject"],
        "optional_fields": ["style", "expression", "background"],
        "defaults": {"style": "3D渲染", "expression": "微笑, 友好", "background": "柔和纯色渐变"},
        "engine": "seedream4", "fallback_engine": "wanx",
        "size": "2K",
    },
    "social_emoji": {
        "name": "表情包",
        "category": "社交",
        "template": (
            "一个可爱的表情包设计：{subject}，"
            "表情为{expression}。"
            "风格：{style_desc}，夸张有趣。"
            "纯色背景，主体突出。"
            "适合作为聊天表情使用。"
        ),
        "required_fields": ["subject", "expression"],
        "optional_fields": ["style"],
        "defaults": {"style": "卡通"},
        "engine": "wanx", "fallback_engine": "seedream4",
        "size": "wanx_1:1",
    },
    "social_card": {
        "name": "金句卡片",
        "category": "社交",
        "template": (
            "一张精致的金句卡片设计。"
            "卡片上用优雅的字体写着：\"{text}\"。"
            "背景：{background}，配色{color_scheme}。"
            "排版精美, 文字清晰可读, 适合社交媒体分享。"
            "整体风格{mood}, 有质感。"
        ),
        "required_fields": ["text"],
        "optional_fields": ["background", "color_scheme", "mood"],
        "defaults": {"background": "渐变色或纹理底", "color_scheme": "温暖和谐", "mood": "文艺"},
        "engine": "wanx", "fallback_engine": "seedream4",
        "size": "wanx_1:1",
    },

    # ── 创意艺术 ──
    "art_general": {
        "name": "艺术创作",
        "category": "创意",
        "template": (
            "{style_desc}风格的艺术作品。"
            "主体：{subject}。"
            "{detail}"
            "{lighting_desc}"
            "画面精致，艺术感强烈，细节丰富。"
        ),
        "required_fields": ["subject"],
        "optional_fields": ["style", "detail"],
        "defaults": {"style": "油画", "detail": ""},
        "engine": "seedream5", "fallback_engine": "seedream4",
        "size": "2K",
    },

    # ── 3D/材质 ──
    "3d_crystal": {
        "name": "水晶质感",
        "category": "3D",
        "template": (
            "一幅照片级真实、细节高度丰富的图像，主体是{subject}，"
            "以清澈、抛光度极高的透明玻璃或水晶材质渲染而成。"
            "所有边缘采用圆润倒角与光滑曲面处理，在光线下产生优雅的折射效果。"
            "主体略微倾斜摆放，仿佛漂浮在洁净的淡米白色棚拍背景上方。"
            "照明为明亮、干净的专业棚拍光，重点突出玻璃材质的透明性、镜面反射与折射特性。"
            "高调光感与浅景深处理，主体保持绝对清晰焦点，背景柔和虚化。"
        ),
        "required_fields": ["subject"],
        "optional_fields": [],
        "defaults": {},
        "engine": "seedream5", "fallback_engine": "seedream4",
        "size": "2K",
    },
    "3d_plush": {
        "name": "毛绒玩具",
        "category": "3D",
        "template": (
            "将{subject}转换成一个柔软蓬松的3D立体物体。"
            "使用原有颜色。该物体完全被毛发覆盖，"
            "拥有超逼真的毛发纹理和柔和的阴影。"
            "它位于干净的浅灰色背景中央，轻柔地漂浮在空中。"
            "风格超现实、触感丰富且现代，营造出舒适和趣味盎然的感觉。"
            "采用摄影棚灯光和高分辨率渲染。"
        ),
        "required_fields": ["subject"],
        "optional_fields": [],
        "defaults": {},
        "engine": "seedream4", "fallback_engine": "seedream5",
        "size": "2K",
    },
    "3d_inflatable": {
        "name": "充气玩具",
        "category": "3D",
        "template": (
            "将{subject}转换成一个可爱的充气玩具风格3D物体。"
            "材质为光滑的气球般质感, 圆润饱满, 色彩鲜艳。"
            "物体略微漂浮在空中, 带有轻微的反光。"
            "背景为干净的纯色, 柔和的环境光照。"
            "整体风格可爱、趣味十足, 高分辨率渲染。"
        ),
        "required_fields": ["subject"],
        "optional_fields": [],
        "defaults": {},
        "engine": "seedream4", "fallback_engine": "seedream5",
        "size": "2K",
    },
    "3d_miniature": {
        "name": "等距微缩房间",
        "category": "3D",
        "template": (
            "一个等距3D立方体微缩房间（浅切面真立方体；所有物品均严格包含在立方体内）。"
            "房间描述：{room_description}。"
            "{character_desc}"
            "光照：{atmosphere}；逼真的反射和彩色阴影。"
            "视角：略微抬高的等距四分之三视角，立方体正面边缘居中；"
            "没有任何元素突出于立方体之外。"
            "照片级材质，细节丰富；中性背景。无水印。"
        ),
        "required_fields": ["room_description"],
        "optional_fields": ["character_desc", "atmosphere"],
        "defaults": {"character_desc": "", "atmosphere": "温暖日光"},
        "engine": "seedream5", "fallback_engine": "seedream4",
        "size": "2K",
    },

    # ── 实用 ──
    "util_weather": {
        "name": "城市天气卡片",
        "category": "实用",
        "template": (
            "呈现一个清晰的45°俯视视角，垂直（9:16）等距3D微缩卡通场景，"
            "突出画面中心{city}的标志性地标，展现精细的建模。"
            "场景采用柔和细腻的纹理，搭配逼真的PBR材质和柔和逼真的光影效果。"
            "天气元素巧妙地融入城市建筑，营造身临其境的天气氛围。"
            "使用简洁统一的构图，采用极简主义美学，柔和纯色背景。"
            "在顶部中心醒目位置显示天气图标，下方显示日期和温度范围。"
            "城市名称({city})位于天气图标正上方。"
        ),
        "required_fields": ["city"],
        "optional_fields": [],
        "defaults": {},
        "engine": "seedream5", "fallback_engine": "seedream4",
        "size": "2K_9:16",
    },
    "util_fridge_magnet": {
        "name": "城市冰箱贴",
        "category": "实用",
        "template": (
            "展示一张清晰的俯视图，照片内容为{city}地标建筑的3D磁贴，"
            "磁贴需整齐排列成平行线和直角，呈小山状。"
            "这些磁贴需为逼真的微缩模型。"
            "在顶部中央放置一个印有城市名称的纪念磁贴，"
            "以及一张手写便签，上面写着温度和天气状况。"
            "所有物品不得重复。"
        ),
        "required_fields": ["city"],
        "optional_fields": [],
        "defaults": {},
        "engine": "seedream5", "fallback_engine": "seedream4",
        "size": "2K",
    },
    "util_food_map": {
        "name": "美食地图",
        "category": "实用",
        "template": (
            "制作一张{region}地图，地图上每个区域都用该区域最著名的食物来构成"
            "（各区域地图上的图案应该看起来像是由食物组成的，而不是食物的图片）。"
            "仔细检查，确保每个区域都正确无误。"
            "色彩丰富, 细节精致, 俯视视角。"
        ),
        "required_fields": ["region"],
        "optional_fields": [],
        "defaults": {},
        "engine": "seedream5", "fallback_engine": "seedream4",
        "size": "2K",
    },
    "util_storyboard": {
        "name": "电影分镜",
        "category": "实用",
        "template": (
            "使用宽屏面板，为{title}创作电影分镜脚本。"
            "场景描述：{scene_description}。"
            "包含{panel_count}个分镜格子，每格标注镜头类型。"
            "风格为黑白铅笔素描，带简要的镜头说明文字。"
        ),
        "required_fields": ["title"],
        "optional_fields": ["scene_description", "panel_count"],
        "defaults": {"scene_description": "开场场景", "panel_count": "6"},
        "engine": "seedream5", "fallback_engine": "seedream4",
        "size": "2K_16:9",
    },

    # ── 设计 ──
    "design_ppt": {
        "name": "PPT页面",
        "category": "设计",
        "template": (
            "一页精美的PPT幻灯片设计。"
            "内容主题：{subject}。"
            "设计风格：{style_desc}。"
            "背景使用{background}，"
            "标题使用优雅的字体，正文使用现代无衬线体。"
            "配色：{color_scheme}。"
            "视觉元素：注重排版的网格布局，关键信息使用卡片布局。"
            "一页一张图，不要将PPT变成一整张图片。"
        ),
        "required_fields": ["subject"],
        "optional_fields": ["style", "background", "color_scheme"],
        "defaults": {"style": "温暖学术", "background": "暖米色/奶油色底色", "color_scheme": "赤陶红+芥末黄+深海军蓝"},
        "engine": "seedream5", "fallback_engine": "seedream4",
        "size": "2K_16:9",
    },
    "design_logo": {
        "name": "Logo设计",
        "category": "设计",
        "template": (
            "一个专业的Logo设计，品牌名称：{brand_name}。"
            "风格：{style_desc}，{mood}。"
            "配色：{color_scheme}。"
            "Logo应简洁易识别，适合多种尺寸使用。"
            "纯白色背景，高清矢量感。"
        ),
        "required_fields": ["brand_name"],
        "optional_fields": ["style", "mood", "color_scheme"],
        "defaults": {"style": "极简现代", "mood": "专业可信赖", "color_scheme": "品牌色"},
        "engine": "wanx", "fallback_engine": "seedream4",
        "size": "wanx_1:1",
    },
    "design_packaging": {
        "name": "包装设计",
        "category": "设计",
        "template": (
            "一个精美的产品包装设计，产品：{subject}。"
            "包装风格：{style_desc}。"
            "展示方式：{display}，能看到包装的多个面。"
            "材质：{material}，印刷品质感。"
            "配色：{color_scheme}，整体协调。"
        ),
        "required_fields": ["subject"],
        "optional_fields": ["style", "display", "material", "color_scheme"],
        "defaults": {"style": "现代简约", "display": "45°角立体展示", "material": "哑光纸盒", "color_scheme": "清新自然"},
        "engine": "seedream5", "fallback_engine": "seedream4",
        "size": "2K",
    },

    # ── 人物 ──
    "person_portrait": {
        "name": "写实肖像",
        "category": "人物",
        "template": (
            "一张专业的人物肖像摄影，{subject}。"
            "光照：{lighting}。"
            "背景：{background}。"
            "表情自然，眼神有神，皮肤纹理真实。"
            "浅景深, 焦点在眼睛上, 专业后期调色。"
        ),
        "required_fields": ["subject"],
        "optional_fields": ["lighting", "background"],
        "defaults": {"lighting": "柔和的窗户侧光, 自然暖调", "background": "虚化的室内环境"},
        "engine": "seedream5", "fallback_engine": "seedream4",
        "size": "2K_3:4",
    },
    "person_action_figure": {
        "name": "手办/模型",
        "category": "人物",
        "template": (
            "一个精致的手办/模型设计，角色：{subject}。"
            "材质为哑光PVC，头部较大，身体较小，Q版/手办风格。"
            "动作：{action}，表情：{expression}。"
            "底座：{base}。"
            "摄影棚灯光，高分辨率，产品级展示。"
        ),
        "required_fields": ["subject"],
        "optional_fields": ["action", "expression", "base"],
        "defaults": {"action": "站立", "expression": "微笑", "base": "圆形黑色底座"},
        "engine": "seedream5", "fallback_engine": "seedream4",
        "size": "2K",
    },

    # ── 场景 ──
    "scene_chalkboard": {
        "name": "黑板粉笔画",
        "category": "场景",
        "template": (
            "将{subject}用粉笔在黑板上绘制，使用彩色粉笔。"
            "从某个角度拍摄了黑板，场景来自{setting}。"
            "黑板靠墙放置，{surroundings}在旁边。"
            "插图旁边用粉笔写着\"{text}\"。"
            "粉笔画风格保持手绘感，有适度的粉笔粉末飞散效果。"
        ),
        "required_fields": ["subject"],
        "optional_fields": ["setting", "surroundings", "text"],
        "defaults": {"setting": "教室", "surroundings": "老师的桌子", "text": ""},
        "engine": "seedream4", "fallback_engine": "seedream5",
        "size": "2K_16:9",
    },

    # ── 电商 ──
    "ecom_main": {
        "name": "电商主图",
        "category": "电商",
        "template": (
            "一张电商产品主图，产品：{subject}。"
            "纯白色背景，产品居中，占画面70%左右。"
            "柔和均匀的布光，消除硬阴影。"
            "产品细节清晰，色彩还原准确。"
            "适合电商平台展示的专业产品照片。"
        ),
        "required_fields": ["subject"],
        "optional_fields": [],
        "defaults": {},
        "engine": "wanx", "fallback_engine": "seedream4",
        "size": "wanx_1:1",
    },
    "ecom_banner": {
        "name": "电商横幅",
        "category": "电商",
        "template": (
            "一张电商促销横幅设计，主题：{subject}。"
            "包含产品展示和{text}促销文字。"
            "配色：{color_scheme}，整体风格{mood}。"
            "横幅比例16:9，适合店铺首页展示。"
        ),
        "required_fields": ["subject"],
        "optional_fields": ["text", "color_scheme", "mood"],
        "defaults": {"text": "限时优惠", "color_scheme": "红金/品牌色", "mood": "热烈喜庆"},
        "engine": "wanx", "fallback_engine": "seedream4",
        "size": "wanx_16:9",
    },

    # ── 内容创作 ──
    "content_xhs": {
        "name": "小红书配图",
        "category": "内容",
        "template": (
            "一张精致的Instagram/小红书风格配图，"
            "主题：{subject}。"
            "整体色调温暖柔和，带有轻微的胶片颗粒感。"
            "构图采用{composition}，突出主体。"
            "画面散发出{mood}的氛围。"
            "色彩倾向于{color_scheme}，搭配自然柔光。"
            "适合社交媒体分享的高质量画面。"
        ),
        "required_fields": ["subject"],
        "optional_fields": ["composition", "mood", "color_scheme"],
        "defaults": {"composition": "居中或三分法", "mood": "温暖治愈", "color_scheme": "奶茶色系/莫兰迪色"},
        "engine": "seedream4", "fallback_engine": "wanx",
        "size": "2K_3:4",
    },
    "content_blog": {
        "name": "文章头图",
        "category": "内容",
        "template": (
            "一张文章头图设计，主题：{subject}。"
            "风格简洁现代，{style_desc}。"
            "画面有足够的留白空间可放置标题文字。"
            "配色：{color_scheme}。"
            "横幅比例16:9, 适合博客/公众号头图。"
        ),
        "required_fields": ["subject"],
        "optional_fields": ["style", "color_scheme"],
        "defaults": {"style": "扁平插画", "color_scheme": "蓝绿渐变"},
        "engine": "wanx", "fallback_engine": "seedream4",
        "size": "wanx_16:9",
    },

    # ── V10.5 新增场景 (YouMind研究) ──
    "info_bento": {
        "name": "Bento产品信息图",
        "category": "信息图",
        "template": (
            "创建一个包含8个模块的高品质液态玻璃Bento网格产品信息图。"
            "产品：{subject}。"
            "调色板源自产品的自然主色调，强调色为完全饱和的主色调。"
            "视觉风格：主打产品以真实摄影呈现，占28-30%面积。"
            "卡片采用Apple液态玻璃效果(85-90%透明)，带极细边框和微妙阴影，营造浮动深度。"
            "背景为产品精髓的柔和焦散或抽象光晕，高度模糊。"
            "8个模块：M1主打产品展示+名称，M2核心优势(4点+图标)，"
            "M3使用方法(4种+图标)，M4关键指标(5个数据点)，"
            "M5适用人群(4推荐+3注意)，M6重要提示(4项+警告图标)，"
            "M7快速参考，M8趣味知识(3个事实)。"
            "不对称Bento网格，16:9横向，超高品质液态玻璃信息图。"
        ),
        "required_fields": ["subject"],
        "optional_fields": [],
        "defaults": {},
        "engine": "seedream5", "fallback_engine": "seedream4",
        "size": "2K_16:9",
    },
    "info_comparison": {
        "name": "对比信息图",
        "category": "信息图",
        "template": (
            "一张精美的对比信息图，主题：{subject}。"
            "左右分栏设计，左侧展示{option_a}，右侧展示{option_b}。"
            "中间用VS或分隔线区分。"
            "每侧包含关键数据、优势图标和简要说明。"
            "整体配色：左侧{color_a}，右侧{color_b}。"
            "底部有总结推荐。16:9横向，信息层次清晰。"
        ),
        "required_fields": ["subject", "option_a", "option_b"],
        "optional_fields": ["color_a", "color_b"],
        "defaults": {"color_a": "蓝色系", "color_b": "橙色系"},
        "engine": "seedream5", "fallback_engine": "seedream4",
        "size": "2K_16:9",
    },
    "social_quote_portrait": {
        "name": "肖像金句卡",
        "category": "社交",
        "template": (
            "一张宽幅引言卡片，上面印有一位名人的肖像。"
            "背景为{bg_color}，引言文字为浅金色衬线字体："
            "\"{text}\"，下方是较小的文字：\"—{author}\"。"
            "文字前面有一个大而柔和的引号。"
            "人物肖像在左侧，文字在右侧。"
            "文字占据图片的三分之二，肖像占据三分之一，"
            "肖像部分带有轻微的渐变过渡效果。"
            "人物描述：{subject}。"
        ),
        "required_fields": ["text"],
        "optional_fields": ["author", "subject", "bg_color"],
        "defaults": {"author": "", "subject": "一位睿智的思想家", "bg_color": "深棕色"},
        "engine": "seedream5", "fallback_engine": "seedream4",
        "size": "2K_16:9",
    },
    "social_youtube_thumb": {
        "name": "YouTube缩略图",
        "category": "社交",
        "template": (
            "一张引人注目的YouTube视频缩略图，16:9比例。"
            "主题：{subject}。"
            "画面左侧是{person_desc}，表情夸张{expression}。"
            "右侧有大号粗体文字\"{title_text}\"，"
            "字体颜色{text_color}，带有描边和阴影效果。"
            "背景使用{background}，整体色彩鲜艳饱和，"
            "视觉冲击力强，能在小尺寸下吸引点击。"
        ),
        "required_fields": ["subject"],
        "optional_fields": ["person_desc", "expression", "title_text", "text_color", "background"],
        "defaults": {"person_desc": "一位博主", "expression": "惊讶张嘴", "title_text": "", "text_color": "黄色", "background": "渐变色+光效"},
        "engine": "seedream5", "fallback_engine": "seedream4",
        "size": "2K_16:9",
    },
    "photo_food": {
        "name": "美食摄影",
        "category": "摄影",
        "template": (
            "一张令人垂涎的美食摄影照片，菜品：{subject}。"
            "45°俯视角度，浅景深对焦在菜品主体。"
            "餐具和摆盘精致考究，{tableware}。"
            "背景为{background}，自然光从窗户侧面照射。"
            "菜品色泽饱满诱人，能看到食物的纹理和热气。"
            "色调温暖，整体氛围{mood}。"
            "专业美食摄影级画质，ISO低，锐度高。"
        ),
        "required_fields": ["subject"],
        "optional_fields": ["tableware", "background", "mood"],
        "defaults": {"tableware": "白色陶瓷餐盘搭配木质餐垫", "background": "木质桌面搭配绿植", "mood": "温馨家常"},
        "engine": "seedream4", "fallback_engine": "seedream5",
        "size": "2K",
    },
    "photo_fashion": {
        "name": "时尚大片",
        "category": "摄影",
        "template": (
            "一张高端时尚杂志级人像大片。"
            "模特描述：{subject}。"
            "穿着{outfit}，姿态{pose}。"
            "拍摄场景：{location}。"
            "光照：{lighting}。"
            "风格参考：Vogue/ELLE杂志封面级别。"
            "浅景深，肤色通透，妆容精致，画面有张力。"
        ),
        "required_fields": ["subject"],
        "optional_fields": ["outfit", "pose", "location", "lighting"],
        "defaults": {"outfit": "高级定制时装", "pose": "自信优雅", "location": "极简主义摄影棚", "lighting": "大型柔光箱主光+轮廓光"},
        "engine": "seedream5", "fallback_engine": "seedream4",
        "size": "2K_3:4",
    },
    "photo_landscape": {
        "name": "风光大片",
        "category": "摄影",
        "template": (
            "一张震撼的风光摄影大片。"
            "场景：{subject}。"
            "时间：{time_of_day}。"
            "天空状态：{sky}。"
            "前景有{foreground}引导视线。"
            "使用超广角16mm镜头感，极大景深，"
            "前景到远景都保持锐利。"
            "色彩丰富层次分明，光影戏剧性。"
            "风光摄影大师级构图，三分法或引导线构图。"
        ),
        "required_fields": ["subject"],
        "optional_fields": ["time_of_day", "sky", "foreground"],
        "defaults": {"time_of_day": "黄金时段日落", "sky": "火烧云", "foreground": "岩石或花丛"},
        "engine": "seedream5", "fallback_engine": "seedream4",
        "size": "2K_16:9",
    },
    "game_character": {
        "name": "游戏角色立绘",
        "category": "游戏",
        "template": (
            "一张高品质游戏角色立绘设计。"
            "角色：{subject}。"
            "职业/类型：{role}。"
            "装备/服饰：{outfit}。"
            "动作姿态：{pose}。"
            "画风：{style_desc}，色彩鲜明，线条流畅。"
            "角色占据画面主体，透明或简洁背景。"
            "适合作为游戏角色选择界面或卡牌使用。"
        ),
        "required_fields": ["subject"],
        "optional_fields": ["role", "outfit", "pose", "style"],
        "defaults": {"role": "战士", "outfit": "精致铠甲", "pose": "战斗准备", "style": "动漫插画"},
        "engine": "seedream5", "fallback_engine": "seedream4",
        "size": "2K_3:4",
    },
    "design_app_mockup": {
        "name": "App界面展示",
        "category": "设计",
        "template": (
            "一张精美的App界面设计展示图。"
            "App类型：{subject}。"
            "展示方式：{display}手机屏幕，{angle}角度。"
            "界面风格：{style_desc}，配色{color_scheme}。"
            "屏幕内容展示{screen_content}。"
            "背景为{background}，整体画面现代精致。"
            "适合产品发布或App Store展示。"
        ),
        "required_fields": ["subject"],
        "optional_fields": ["display", "angle", "style", "color_scheme", "screen_content", "background"],
        "defaults": {"display": "iPhone 16 Pro", "angle": "微微倾斜15°", "style": "极简主义", "color_scheme": "蓝紫渐变", "screen_content": "核心功能页面", "background": "柔和渐变色"},
        "engine": "seedream5", "fallback_engine": "seedream4",
        "size": "2K",
    },
    "content_comic": {
        "name": "漫画条",
        "category": "内容",
        "template": (
            "一条4格漫画，主题：{subject}。"
            "第1格：{panel1}。"
            "第2格：{panel2}。"
            "第3格：{panel3}。"
            "第4格：{panel4}。"
            "风格：{style_desc}，线条清晰，表情夸张有趣。"
            "每格有对话气泡，排版从左到右。"
            "整体横向排列，16:9比例。"
        ),
        "required_fields": ["subject"],
        "optional_fields": ["panel1", "panel2", "panel3", "panel4", "style"],
        "defaults": {"panel1": "日常场景", "panel2": "发生转折", "panel3": "出乎意料", "panel4": "搞笑结尾", "style": "漫画"},
        "engine": "seedream5", "fallback_engine": "seedream4",
        "size": "2K_16:9",
    },
    "design_invitation": {
        "name": "邀请函/贺卡",
        "category": "设计",
        "template": (
            "一张精美的{occasion}邀请函/贺卡设计。"
            "主要文字：\"{text}\"。"
            "风格：{style_desc}。"
            "装饰元素：{decoration}。"
            "配色：{color_scheme}。"
            "整体典雅有质感，印刷级品质。"
            "竖版3:4比例。"
        ),
        "required_fields": ["occasion"],
        "optional_fields": ["text", "style", "decoration", "color_scheme"],
        "defaults": {"text": "", "style": "现代典雅", "decoration": "花卉与金色线条", "color_scheme": "白色+金色+深绿"},
        "engine": "seedream4", "fallback_engine": "wanx",
        "size": "2K_3:4",
    },
    "design_book_cover": {
        "name": "书籍封面",
        "category": "设计",
        "template": (
            "一个专业的书籍封面设计。"
            "书名：\"{title}\"。"
            "作者：{author}。"
            "类型：{genre}。"
            "视觉主体：{subject}。"
            "风格：{style_desc}。"
            "封面构图：上方留白放书名(大号字体)，"
            "中间为视觉主体，底部放作者名。"
            "整体有文学气质，适合出版物使用。竖版2:3比例。"
        ),
        "required_fields": ["title"],
        "optional_fields": ["author", "genre", "subject", "style"],
        "defaults": {"author": "", "genre": "文学", "subject": "抽象艺术元素", "style": "极简主义"},
        "engine": "seedream5", "fallback_engine": "seedream4",
        "size": "2K_3:4",
    },
    "social_carousel": {
        "name": "社媒轮播图",
        "category": "社交",
        "template": (
            "一张适合社交媒体轮播的知识卡片，序号{slide_num}。"
            "主题：{subject}。"
            "本页内容：{content}。"
            "风格：{style_desc}，配色{color_scheme}。"
            "排版：大标题+分点内容+配图/图标。"
            "1:1正方形比例，文字清晰可读，"
            "视觉统一，适合Instagram/小红书轮播。"
        ),
        "required_fields": ["subject", "content"],
        "optional_fields": ["slide_num", "style", "color_scheme"],
        "defaults": {"slide_num": "1/5", "style": "现代简约", "color_scheme": "柔和渐变"},
        "engine": "wanx", "fallback_engine": "seedream4",
        "size": "wanx_1:1",
    },
    "content_meme": {
        "name": "表情包/梗图",
        "category": "内容",
        "template": (
            "一张搞笑的网络梗图/表情包。"
            "场景：{subject}。"
            "上方文字：\"{top_text}\"。"
            "下方文字：\"{bottom_text}\"。"
            "风格：{style_desc}，表情夸张，画面简洁有趣。"
            "白色粗体Impact字体，带黑色描边。"
            "1:1正方形比例。"
        ),
        "required_fields": ["subject"],
        "optional_fields": ["top_text", "bottom_text", "style"],
        "defaults": {"top_text": "", "bottom_text": "", "style": "写实搞笑"},
        "engine": "wanx", "fallback_engine": "seedream4",
        "size": "wanx_1:1",
    },

    # ── 浮世绘闪卡 (特色模板) ──
    "art_ukiyoe_card": {
        "name": "浮世绘闪卡",
        "category": "创意",
        "template": (
            "一张日式浮世绘风格的收藏级集换式卡牌设计，竖构图。"
            "插画风格紧密模仿传统浮世绘的视觉美学："
            "粗细变化的墨笔轮廓线、传统木版画的配色方案，以及戏剧性的动态构图。"
            "卡牌主角是{subject}，处于{pose}。"
            "周围环绕着{visual_effects}，以传统日式水墨画风格呈现。"
            "背景融合纹理化的镭射闪卡效果，在传统水墨元素下方闪烁。"
            "边框为日本传统纹样（青海波或麻叶纹）组成的装饰性边框。"
            "底部有风格化横幅，上面用古朴的书法写着\"{text}\"。"
        ),
        "required_fields": ["subject"],
        "optional_fields": ["pose", "visual_effects", "text"],
        "defaults": {"pose": "动态战斗姿势", "visual_effects": "火焰和能量光环", "text": ""},
        "engine": "seedream5", "fallback_engine": "seedream4",
        "size": "2K_3:4",
    },
}

# 场景别名映射
SCENE_ALIASES = {
    "名片": "biz_card", "business card": "biz_card", "card": "biz_card",
    "海报": "biz_poster", "poster": "biz_poster", "宣传图": "biz_poster",
    "产品图": "biz_product", "产品照": "biz_product", "product": "biz_product",
    "头像": "social_avatar", "avatar": "social_avatar", "ip": "social_avatar", "ip形象": "social_avatar",
    "表情包": "social_emoji", "emoji": "social_emoji", "sticker": "social_emoji",
    "金句": "social_card", "卡片": "social_card", "quote": "social_card",
    "水晶": "3d_crystal", "crystal": "3d_crystal", "玻璃": "3d_crystal",
    "毛绒": "3d_plush", "plush": "3d_plush", "蓬松": "3d_plush",
    "充气": "3d_inflatable", "inflatable": "3d_inflatable",
    "微缩": "3d_miniature", "miniature": "3d_miniature", "等距": "3d_miniature", "小房间": "3d_miniature",
    "天气": "util_weather", "weather": "util_weather",
    "冰箱贴": "util_fridge_magnet", "fridge": "util_fridge_magnet",
    "美食地图": "util_food_map", "food map": "util_food_map",
    "分镜": "util_storyboard", "storyboard": "util_storyboard",
    "ppt": "design_ppt", "幻灯片": "design_ppt",
    "logo": "design_logo", "标志": "design_logo",
    "包装": "design_packaging", "packaging": "design_packaging",
    "肖像": "person_portrait", "portrait": "person_portrait", "写真": "person_portrait",
    "手办": "person_action_figure", "模型": "person_action_figure", "figure": "person_action_figure",
    "黑板": "scene_chalkboard", "粉笔画": "scene_chalkboard",
    "电商": "ecom_main", "主图": "ecom_main", "白底图": "ecom_main",
    "横幅": "ecom_banner", "banner": "ecom_banner",
    "小红书": "content_xhs", "xhs": "content_xhs",
    "头图": "content_blog", "blog": "content_blog", "公众号": "content_blog",
    "浮世绘": "art_ukiyoe_card",
    "信息图": "info_bento", "bento": "info_bento", "产品介绍": "info_bento",
    "对比图": "info_comparison", "vs": "info_comparison", "对比": "info_comparison",
    "金句卡": "social_quote_portrait", "引言卡": "social_quote_portrait", "quote card": "social_quote_portrait",
    "缩略图": "social_youtube_thumb", "youtube": "social_youtube_thumb", "封面图": "social_youtube_thumb",
    "美食": "photo_food", "食物": "photo_food", "food": "photo_food", "菜品": "photo_food",
    "时尚": "photo_fashion", "fashion": "photo_fashion", "模特": "photo_fashion",
    "风景": "photo_landscape", "landscape": "photo_landscape", "风光": "photo_landscape",
    "游戏角色": "game_character", "立绘": "game_character", "角色": "game_character",
    "app": "design_app_mockup", "界面": "design_app_mockup", "mockup": "design_app_mockup",
    "漫画条": "content_comic", "四格漫画": "content_comic", "comic": "content_comic",
    "邀请函": "design_invitation", "贺卡": "design_invitation", "请柬": "design_invitation",
    "封面": "design_book_cover", "book": "design_book_cover", "书封": "design_book_cover",
    "轮播": "social_carousel", "carousel": "social_carousel", "知识卡片": "social_carousel",
    "梗图": "content_meme", "meme": "content_meme",
}


def resolve_scene(scene_hint: str) -> dict | None:
    """根据场景提示解析模板"""
    if not scene_hint:
        return None
    hint = scene_hint.strip().lower()
    # 直接ID匹配
    if hint in SCENE_TEMPLATES:
        return SCENE_TEMPLATES[hint]
    # 别名匹配
    scene_id = SCENE_ALIASES.get(hint)
    if scene_id and scene_id in SCENE_TEMPLATES:
        return SCENE_TEMPLATES[scene_id]
    # 模糊匹配
    for alias, sid in SCENE_ALIASES.items():
        if alias in hint or hint in alias:
            return SCENE_TEMPLATES.get(sid)
    return None


# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# 通用负面提示词
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

BASE_NEGATIVE = "低分辨率，低画质，肢体畸形，手指畸形，蜡像感，构图混乱，文字模糊"

QUALITY_NEGATIVE = {
    "high": "低分辨率，低画质，肢体畸形，手指畸形，画面过饱和，蜡像感，人脸无细节，过度光滑，AI感明显，构图混乱，文字模糊扭曲，噪点，模糊，过曝，欠曝",
    "standard": BASE_NEGATIVE,
}

QUALITY_SUFFIX = {
    "high": {
        "zh": "，高清细腻，光影层次丰富，构图精美，色彩和谐，专业摄影级画质，8K超高清",
        "en": ", ultra high definition, rich lighting and shadows, exquisite composition, harmonious colors, professional photography quality, 8K UHD",
    },
    "standard": {
        "zh": "，画面清晰，构图合理",
        "en": ", clear image, good composition",
    },
}


# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# Prompt Builder
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

def build_prompt(scene_id: str | None, style_name: str | None,
                 fields: dict, quality: str = "high") -> dict:
    """构建结构化prompt

    返回:
        {
            "prompt": str,          # 最终prompt
            "negative": str,        # 负面提示词
            "engine": str,          # 推荐引擎
            "fallback_engine": str, # 备选引擎
            "size": str,            # 推荐尺寸
            "scene_name": str,      # 场景名称
        }
    """
    scene = None
    if scene_id:
        scene = SCENE_TEMPLATES.get(scene_id) or resolve_scene(scene_id)

    style = resolve_style(style_name) if style_name else None

    # ── 有模板: 填充模板 ──
    if scene:
        template = scene["template"]
        # 准备字段 (defaults + user fields)
        merged = dict(scene.get("defaults", {}))
        merged.update({k: v for k, v in fields.items() if v})

        # 风格描述注入
        if style:
            merged.setdefault("style_desc", style["keywords"])
            merged.setdefault("lighting_desc", style["lighting"])
        else:
            merged.setdefault("style_desc", merged.get("style", "精美"))
            merged.setdefault("lighting_desc", "柔和自然光")

        # 处理名片风格变体
        if scene_id == "biz_card" and "style_variants" in scene:
            variant_style = merged.get("style", "赛博朋克")
            merged["style_detail"] = scene["style_variants"].get(
                variant_style, scene["style_variants"].get("赛博朋克", ""))

        # 填充模板
        try:
            prompt = template.format(**merged)
        except KeyError as e:
            # 缺少字段时, 用空字符串填充
            import re
            prompt = re.sub(r'\{(\w+)\}', lambda m: merged.get(m.group(1), ''), template)

        # 负面提示
        negative_parts = [QUALITY_NEGATIVE.get(quality, BASE_NEGATIVE)]
        if style and style.get("negative"):
            negative_parts.append(style["negative"])
        negative = "，".join(negative_parts)

        return {
            "prompt": prompt,
            "negative": negative,
            "engine": scene.get("engine", "seedream4"),
            "fallback_engine": scene.get("fallback_engine", "wanx"),
            "size": scene.get("size", "2K"),
            "scene_name": scene["name"],
        }

    # ── 无模板: 通用构建 ──
    subject = fields.get("subject", "")
    prompt_parts = []

    if style:
        prompt_parts.append(f"{style['keywords']}风格的作品。")
        prompt_parts.append(f"主体：{subject}。")
        prompt_parts.append(f"光照：{style['lighting']}。")
    else:
        prompt_parts.append(subject)

    # 质量后缀
    has_chinese = any('\u4e00' <= c <= '\u9fff' for c in subject)
    lang = "zh" if has_chinese else "en"
    suffix = QUALITY_SUFFIX.get(quality, QUALITY_SUFFIX["standard"])[lang]
    if "8K" not in subject and "高清" not in subject:
        prompt_parts.append(suffix)

    prompt = "".join(prompt_parts)

    negative_parts = [QUALITY_NEGATIVE.get(quality, BASE_NEGATIVE)]
    if style and style.get("negative"):
        negative_parts.append(style["negative"])
    negative = "，".join(negative_parts)

    return {
        "prompt": prompt,
        "negative": negative,
        "engine": "seedream4",
        "fallback_engine": "wanx",
        "size": "2K",
        "scene_name": "通用",
    }


def list_scenes() -> list[dict]:
    """列出所有可用场景模板"""
    result = []
    for sid, scene in SCENE_TEMPLATES.items():
        result.append({
            "id": sid,
            "name": scene["name"],
            "category": scene["category"],
            "required_fields": scene["required_fields"],
            "engine": scene["engine"],
            "size": scene["size"],
        })
    return result


def list_styles() -> list[str]:
    """列出所有可用风格"""
    return list(STYLE_DICT.keys())


# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# V10.5 智能Prompt美化器 (啊哈时刻)
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

_BEAUTIFY_DIMENSIONS = {
    "composition": [
        "三分法构图，主体位于黄金分割点",
        "居中对称构图，庄重感",
        "对角线引导构图，动态感",
        "前中后三层景深，空间感",
        "留白构图，呼吸感",
        "框架构图，通过自然框架聚焦主体",
    ],
    "lighting": [
        "黄金时段侧光，温暖长影",
        "柔和的窗户自然光，逆光轮廓",
        "戏剧性伦勃朗光，明暗对比强烈",
        "环境光+补光，柔和无硬阴影",
        "顶光+反射板，立体感",
        "丁达尔光线穿透，神圣氛围",
    ],
    "color_mood": [
        "暖色调为主，橙红金，温馨治愈",
        "冷色调为主，蓝紫银，静谧高级",
        "莫兰迪低饱和，灰调优雅",
        "高饱和鲜艳，活力张扬",
        "黑金配色，奢华质感",
        "马卡龙糖果色，甜美少女",
    ],
    "texture_detail": [
        "细腻肌理，可见微观纹理",
        "光滑丝绒质感，柔和过渡",
        "粗粝质感，颗粒与磨砂",
        "透明玻璃质感，折射光泽",
        "金属磨砂质感，冷冽高级",
        "有机自然纹理，木纹/石纹/水纹",
    ],
    "atmosphere": [
        "氛围感粒子，空气中的微尘/光斑",
        "薄雾弥漫，梦幻朦胧",
        "雨后清新，水珠反光",
        "夜景城市灯光，光斑散景",
        "阳光穿过树叶的斑驳光影",
        "飘落的花瓣/落叶/雪花",
    ],
}

_BEAUTIFY_QUALITY_TAGS = {
    "photo": "专业摄影级画质，超高分辨率，锐度极高，ISO 100，浅景深散焦光斑",
    "art": "大师级艺术品质，笔触/纹理细腻，色彩层次丰富，构图精美",
    "3d": "影视级3D渲染，PBR材质，全局光照，抗锯齿，高多边形",
    "design": "商业级设计品质，印刷级锐度，色彩管理精确，排版考究",
}

import re as _re
import hashlib as _hashlib


def beautify_prompt(raw_prompt: str, quality: str = "high",
                    seed: int | None = None) -> dict:
    """V10.5 智能Prompt美化器 — 将简单用户输入升级为专业级提示词

    核心理念: 用户说"画一只猫", 我们生成一段包含构图/光影/色调/纹理/氛围
    的丰富描述, 让用户体验到"啊哈, 原来AI可以画得这么好"的惊喜时刻。

    Args:
        raw_prompt: 用户原始输入
        quality: "high" or "standard"
        seed: 可选随机种子(用于稳定测试)

    Returns:
        {
            "original": str,       # 原始输入
            "beautified": str,     # 美化后的prompt
            "negative": str,       # 负面提示词
            "enhancements": list,  # 添加的增强维度
        }
    """
    import random
    if seed is not None:
        rng = random.Random(seed)
    else:
        h = int(_hashlib.md5(raw_prompt.encode()).hexdigest()[:8], 16)
        rng = random.Random(h)

    prompt = raw_prompt.strip()
    if not prompt:
        return {"original": "", "beautified": "", "negative": "", "enhancements": []}

    has_chinese = any('\u4e00' <= c <= '\u9fff' for c in prompt)
    is_already_detailed = len(prompt) > 150 or any(
        kw in prompt for kw in ["构图", "光照", "色调", "景深", "composition",
                                 "lighting", "bokeh", "depth of field"]
    )

    if is_already_detailed:
        suffix = QUALITY_SUFFIX.get(quality, QUALITY_SUFFIX["standard"])
        lang = "zh" if has_chinese else "en"
        if "8K" not in prompt and "高清" not in prompt and "UHD" not in prompt:
            prompt += suffix[lang]
        return {
            "original": raw_prompt,
            "beautified": prompt,
            "negative": QUALITY_NEGATIVE.get(quality, BASE_NEGATIVE),
            "enhancements": ["quality_suffix"],
        }

    enhancements = []
    parts = [prompt]

    category = _detect_category(prompt)

    comp = rng.choice(_BEAUTIFY_DIMENSIONS["composition"])
    parts.append(comp)
    enhancements.append(f"构图: {comp}")

    light = rng.choice(_BEAUTIFY_DIMENSIONS["lighting"])
    parts.append(light)
    enhancements.append(f"光照: {light}")

    color = rng.choice(_BEAUTIFY_DIMENSIONS["color_mood"])
    parts.append(color)
    enhancements.append(f"色调: {color}")

    if quality == "high":
        texture = rng.choice(_BEAUTIFY_DIMENSIONS["texture_detail"])
        parts.append(texture)
        enhancements.append(f"质感: {texture}")

        atmo = rng.choice(_BEAUTIFY_DIMENSIONS["atmosphere"])
        parts.append(atmo)
        enhancements.append(f"氛围: {atmo}")

    quality_tag = _BEAUTIFY_QUALITY_TAGS.get(category, _BEAUTIFY_QUALITY_TAGS["photo"])
    parts.append(quality_tag)
    enhancements.append(f"品质: {category}")

    sep = "，" if has_chinese else ", "
    beautified = sep.join(parts)

    return {
        "original": raw_prompt,
        "beautified": beautified,
        "negative": QUALITY_NEGATIVE.get(quality, BASE_NEGATIVE),
        "enhancements": enhancements,
    }


def _detect_category(prompt: str) -> str:
    """检测prompt的类别以选择合适的品质标签"""
    p = prompt.lower()
    if any(kw in p for kw in ["3d", "微缩", "水晶", "毛绒", "充气", "乐高", "黏土",
                               "render", "模型", "手办"]):
        return "3d"
    if any(kw in p for kw in ["设计", "logo", "ppt", "海报", "名片", "包装", "ui",
                               "app", "banner", "排版", "封面", "邀请"]):
        return "design"
    if any(kw in p for kw in ["画", "绘", "水墨", "油画", "水彩", "漫画", "插画",
                               "吉卜力", "像素", "浮世绘", "艺术"]):
        return "art"
    return "photo"


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (MEDIUM)