Skill: zhaobod1/huo15-img-test (version 2.3.0)

--- name: huo15-img-test displayName: 火一五文生图提示词 description: 火一五文生图提示词 v2.3 — 文生图&视频六件套：(1) enhance_prompt.py 文生图：88 风格预设 + 混合预设 (-p A+B --mix 0.6) + 五锁一致性 + 角色设定图 + 系列批量 + basic/pro/master 三档；(2) enhance_video.py 视频提示词：9 模型规格（Sora/Kling/Runway/Pika/Luma/Hailuo/即梦/Wan）+ 30 镜头运动 + 关键帧三段式；(3) reverse_prompt.py 参考图反解：A1111/ComfyUI/NovelAI metadata 自动识别 + VLM fallback；(4) render_prompt.py 直出图片：ComfyUI/SD-WebUI/DALL-E 三后端；(5) claude_polish.py ⭐v2.3 Claude API 智能润色：把粗糙描述转专业摄影/绘画术语 + 推荐预设/相机/光影；(6) safety_lint.py ⭐v2.3 平台合规润色：识别会被 SD/MJ/DALL-E 误判的艺术词汇并给出合法艺术化替代（仅服务合规艺术创作，拒绝违法/未成年/真人色情）。适配 Midjourney/SD/SDXL/Flux/DALL-E 3。触发词：文生图、火一五文生图提示词、文生视频、提示词、生成图片、生成视频、img-test、text to image、text to video、enhance prompt、提示词增强、图片一致性、系列图、角色一致、批量出图、混合风格、原神+敦煌、参考图反解、reverse prompt、提示词反解、ComfyUI 直出、SD WebUI、DALL-E、视频提示词、Sora、可灵、Runway、即梦、Hailuo、Claude 润色、智能润色、平台合规、艺术化重写。 version: 2.3.0 aliases: - 火一五文生图提示词 - 火一五文生图技能 - 火一五文生视频技能 - 火一五提示词技能 - 火一五提示词全家桶技能 - 火一五AI绘画技能 - 文生图 - 文生视频 - 提示词增强 - 智能润色 - 平台合规润色 - img-test --- # 火一五文生图提示词 v2.3 **一句话描述 → 贴合需求、一致性强的专业 T2I/T2V 提示词，并可直出。** ## v2.3 = 六件套 | 脚本 | 作用 | 一行 demo | |------|------|-----------| | `enhance_prompt.py` | 文生图（升级混合 + polish + safety） | `enhance_prompt.py "持剑女侠" -p "赛博朋克+水墨" --mix 0.6` | | `enhance_video.py` | 视频提示词 | `enhance_video.py "汉服少女转身回眸" -p 汉服写真 -m Kling --duration 6` | | `reverse_prompt.py` | 参考图反解 | `reverse_prompt.py img.png --mj` | | `render_prompt.py` | 直出图片 | `render_prompt.py "原神少女" -p 原神 --backend sd-webui` | | `claude_polish.py` ⭐v2.3 | Claude API 智能润色 | `claude_polish.py "一个温柔的女孩" --pipe` | | `safety_lint.py` ⭐v2.3 | 平台合规润色 | `safety_lint.py "战士手中沾满鲜血的剑" --target dalle` | ## 版本演进 | 维度 | v2.0 | v2.1 | v2.2 | v2.3 | |------|------|------|------|------| | **风格预设** | 56 | 88 | + 混合预设 | 沿用 | | **一致性** | 四锁 + seed + 系列 | + 角色设定图 | + motion 第六锁 | 沿用 | | **贴近需求** | 意图+构图+情绪 | + 时间/天气/季节/负向 | + 视频镜头运动+关键帧 | + **Claude API 润色** | | **生态闭环** | 仅 prompt | 仅 prompt | + 反解 + 直出 | + **平台合规重写** | | **AI 联动** | 无 | 无 | 无 | + **Claude/GPT/Gemini 协作** | ## 使用方式 ### Agent 调用（推荐） ``` 用户: 帮我出一张赛博朋克街头的图 ``` Agent 识别到"赛博朋克"触发词，自动调用： ```bash ~/workspace/projects/openclaw/huo15-skills/huo15-img-test/scripts/enhance_prompt.py \ "赛博朋克街头" -p 赛博朋克 -m Midjourney ``` ### 直接调用 ```bash cd ~/workspace/projects/openclaw/huo15-skills/huo15-img-test # 基础：指定预设 ./scripts/enhance_prompt.py "一只猫" -p 动漫 -m Midjourney # 自动意图（无需 -p，脚本从关键词推断） ./scripts/enhance_prompt.py "为咖啡品牌设计一个logo" # → 自动选 Logo设计, 1:1 ./scripts/enhance_prompt.py "产品白底图：无线耳机" # → 自动选 产品摄影, 1:1 ./scripts/enhance_prompt.py "微距 一滴露珠" # → 自动选 微距摄影, 1:1 # 系列一致性（4 张共享 seed + camera/lighting/palette 锁） ./scripts/enhance_prompt.py "红发女侠" -p 动漫 -s 4 \ --variations "持剑站立,骑马奔驰,弯弓射箭,与龙对视" \ -m Midjourney # 英文别名 + 多模型输出 ./scripts/enhance_prompt.py "spaceship in nebula" -p scifi -m Flux -a 21:9 ./scripts/enhance_prompt.py "minimalist camellia logo" -p logo -m SDXL # JSON 输出（便于集成） ./scripts/enhance_prompt.py "森林少女" -p ghibli -j ``` ## 88 款风格预设 ### 【摄影 · 13】 写实摄影 / 胶片摄影 / 黑白摄影 / 人像摄影 / 时尚大片 / 美食摄影 / 产品摄影 / 微距摄影 / 航拍摄影 / 街拍纪实 / **暗黑美食 · 日杂 · 街头潮流** ⭐v2.1 ### 【动漫 · 10】 动漫 / 新海诚 / 宫崎骏 / 美漫 / Q版 / 童话绘本 / **萌系 · 厚涂 · 轻小说封面 · 赛璐璐** ⭐v2.1 ### 【插画 · 7】 水彩 / 油画 / 水墨 / 工笔国画 / 浮世绘 / 线稿 / 像素艺术 ### 【3D · 7】 3DC4D / 盲盒手办 / 低多边形 / 等距视图 / 粘土 / 毛毡手工 / 纸艺 ### 【设计 · 15】 极简主义 / 平面设计 / Logo设计 / 图标设计 / 信息图 / 品牌KV / 专辑封面 / 复古海报 / 电影海报 / 表情包 / **玻璃拟态 · 新拟态 · 孟菲斯 · 杂志编排 · 包豪斯 · 奶油风** ⭐v2.1 ### 【艺术史 · 4】 印象派 / 后印象派 / 新艺术 / 装饰艺术 ### 【场景氛围 · 17】 赛博朋克 / 蒸汽朋克 / 科幻 / 奇幻 / 黑暗奇幻 / 国潮 / Y2K / Vaporwave / 霓虹灯牌 / 建筑可视化 / 电影感 / 概念艺术 / **粗野主义 · 北欧极简 · 侘寂 · 疗愈治愈 · 美式复古** ⭐v2.1 ### 【游戏艺术 · 7】⭐ v2.1 新类 原神 / 崩铁星穹 / 英雄联盟 / 暗黑4 / Valorant / Pokemon / 暴雪风 ### 【东方传统 · 7】⭐ v2.1 新类 敦煌壁画 / 青花瓷 / 民国月份牌 / 年画 / 剪纸 / 和风 / 汉服写真 > 英文别名支持：`anime`、`ghibli`、`shinkai`、`cyberpunk`、`steampunk`、`scifi`、`minimal`、`logo`、`icon`、`3d`、`c4d`、`octane`、`isometric`、`vangogh`、`artdeco`、`neon`、`vapor`、`y2k`、`genshin`、`lol`、`diablo`、`valorant`、`pokemon`、`dunhuang`、`hanfu`、`wafu`、`glassmorphism`、`neumorphism`、`memphis`、`bauhaus`、`brutalism`、`nordic`、`wabisabi`、`healing`、`cozy`、`americana`、`darkfood`、`muji`、`streetwear`… 运行 `./scripts/enhance_prompt.py -l` 查看完整列表。 ## 参数说明 | 参数 | 作用 | 示例 | |------|------|------| | `subject` | 主体描述（必填） | `"一只猫"` | | `-p, --preset` | 风格预设（中文 / 英文别名） | `-p 赛博朋克` / `-p cyberpunk` | | `-m, --model` | 目标模型 | `Midjourney` / `SD` / `SDXL` / `Flux` / `DALL-E` / `通用` | | `-a, --aspect` | 画幅 | `1:1` / `3:4` / `16:9` / `21:9` / `9:16` | | `-t, --tier` ⭐v2.1 | 质量档位 | `basic` / `pro`(默认) / `master` | | `-cs, --character-sheet` ⭐v2.1 | 角色设定图 T-pose 多视图 | - | | `--avoid` ⭐v2.1 | 额外负面词，逗号分隔 | `--avoid "cluttered, people"` | | `--mood` | 情绪覆盖（不给则从主体自动抽） | `--mood 神秘` | | `--composition` | 构图覆盖 | `--composition 俯拍` | | `--seed` | 种子（不给则按 subject+preset 哈希生成稳定 seed） | `--seed 42` | | `-s, --series` | 系列张数 | `-s 4` | | `--variations` | 系列变体，逗号分隔 | `--variations "A,B,C,D"` | | `-l, --list` | 列出所有预设 | - | | `-j, --json` | JSON 输出 | - | ## 自动抽词（v2.1 扩展） 脚本会从主体描述中自动识别以下字段，无需显式参数： | 维度 | 关键词示例 | |------|-----------| | **意图** | logo / 产品 / 海报 / 头像 / 美食 / 汉服 / 敦煌 / 原神 / 玻璃拟态 ... | | **构图** | 特写 / 近景 / 中景 / 全身 / 俯拍 / 仰拍 / 鸟瞰 / 航拍 / 侧面 / 背面 | | **情绪** | 温暖 / 冷峻 / 神秘 / 梦幻 / 欢快 / 忧郁 / 史诗 / 高级 / 治愈 / 浪漫 ⭐v2.1：紧张 | | **时间** ⭐v2.1 | 清晨 / 早晨 / 正午 / 下午 / 黄昏 / 日落 / 夜晚 / 深夜 / 黎明 / 蓝调时刻 | | **天气** ⭐v2.1 | 晴天 / 多云 / 阴天 / 下雨 / 雨天 / 大雨 / 下雪 / 雪天 / 雾天 / 风暴 / 雷雨 | | **季节** ⭐v2.1 | 春/夏/秋/冬 / 樱花季 / 枫叶季 | | **负向需求** ⭐v2.1 | 不要X / 没有X / 避免X / no X / avoid X / without X → 自动入负面 | ## 一致性四锁（核心机制） 每个预设内置以下锁项，所有系列张图共享 ⇒ 风格漂移大幅下降： | 锁项 | 作用 | 示例（赛博朋克） | |------|------|----------------| | `camera` | 镜头焦段 / 视角 | `low angle wide, 24mm anamorphic` | | `lighting` | 光源 / 光质 | `neon magenta and cyan rim, wet reflective streets` | | `palette` | 色板 | `magenta cyan black, neon highlights` | | `aspect` | 画幅 | `21:9` | 系列模式 (`-s N --variations ...`) 额外锁定 **seed**，变换仅发生在主体描述，框架完全不变。 ## 模型适配细节 | 模型 | 输出格式 | 特有提示 | |------|---------|---------| | **Midjourney** | `主体, 风格, 光影, 色板, 画质 --ar X:Y --stylize 250` | `--cref <url>` 锁角色、`--sref <url>` 锁风格图 | | **Stable Diffusion** | `(subject:1.2), 风格, ..., 质量` + 负面 | 权重语法 `(word:1.3)`、减弱 `[word]`、DPM++ 2M Karras | | **SDXL** | 同 SD，尺寸建议 `1024x1024 / 1216x832 / 1536x640 ...` | Refiner 0.2-0.3 | | **DALL-E 3** | 自然语言段落（已内化负面） | 连续对话中用 "same character / same scene" | | **Flux** | 长句描述 | guidance 3.5（Dev） / 0（Schnell） | | **通用** | 逗号分隔 tags | 三大模型通用骨架 | ## 完整示例 ```bash ./scripts/enhance_prompt.py "一只戴墨镜的猫在霓虹街头" -p 赛博朋克 -m Midjourney ``` 输出： ``` 📌 原始描述 : 一只戴墨镜的猫在霓虹街头 🎨 风格预设 : 赛博朋克 🤖 目标模型 : Midjourney 📐 画幅 : 21:9 🎲 种子建议 : 1873940236 ✅ 正向提示词： 一只戴墨镜的猫在霓虹街头, cyberpunk, neon-soaked, blade runner aesthetic, megacity dystopia, holographic ads, low angle wide, 24mm anamorphic, neon magenta and cyan rim, wet reflective streets, magenta cyan black, neon highlights, detailed cyberpunk cityscape, rainy night ambiance, masterpiece, best quality, ultra detailed, 8k --ar 21:9 --stylize 250 ❌ 负向提示词： --no rustic, medieval, natural countryside, low quality, worst quality, ... 🔒 一致性锁： camera : low angle wide, 24mm anamorphic lighting: neon magenta and cyan rim, wet reflective streets palette : magenta cyan black, neon highlights aspect : 21:9 💡 Midjourney tips： • 角色/产品系列一致：加 --cref <url> 或 --sref <url> • 想要更风格化加 --stylize 500~750；更写实降到 --stylize 50 • 建议 seed 锁定：--seed 1873940236 ``` ## v2.3 新功能 ⭐ ### 5. Claude API 智能润色 `--polish` ```bash # 直接润色（独立调用） export ANTHROPIC_API_KEY=sk-ant-xxx ./scripts/claude_polish.py "一个温柔的女孩在花丛中" ./scripts/claude_polish.py "敦煌神女" --pipe # 输出可直接喂给 enhance_prompt.py 的命令 # 在 enhance_prompt.py 里串联使用（润色 → 88 预设 → 输出） ./scripts/enhance_prompt.py "一个温柔的女孩在花丛中" --polish ./scripts/enhance_prompt.py "雪山下的小屋" --polish --safety MJ -m Midjourney ``` 利用 Claude prompt engineering 优势： - **Prompt caching**：system prompt 用 ephemeral cache，省 90% input token - **Prefill `{`**：assistant 起手 `{` 强制 JSON 输出，无需 tool use - **XML 思维链**：让 Claude 内部分步骤（refine/style/camera/safety/negatives） - **88 预设嵌入 system**：Claude 从清单里挑，不凭记忆 - **零 SDK 依赖**：纯 urllib，避免企业扫描器拦截 anthropic 包 ### 6. 平台合规润色 `--safety` **只做合法艺术创作的平台误判规避，不做 jailbreak。** ```bash # 独立调用 ./scripts/safety_lint.py "战士手中沾满鲜血的剑" --target dalle ./scripts/safety_lint.py "古典维纳斯雕像 nude figure" --target MJ --apply ./scripts/safety_lint.py "如何制作炸弹" # 命中红线 → exit 2 # 在 enhance_prompt.py 里串联 ./scripts/enhance_prompt.py "古风战场鲜血飞溅" --safety dalle ./scripts/enhance_prompt.py "黑暗骑士斩杀恶魔" --safety MJ -p 黑暗奇幻 ``` **红线（直接拒答）**： - ✗ CSAM（未成年 + 性化任意组合） - ✗ 真人 + 色情/政治污蔑 - ✗ 武器/毒品/爆炸物**制作方法/教程** - ✗ 自残/自杀**方法诱导** **黄区（艺术化重写）**： | 类别 | 例子 | 重写策略 | |------|------|----------| | violence | 血、伤口、kill、weapon | crimson splash / battle-scarred / vanquish / ceremonial blade | | nudity | 裸、naked、sexy | classical figure study / fine art reference / fashion editorial | | horror | horror、gore、demon | gothic atmospheric tension / mythical creature | | death | dead、skeleton、skull | memento mori / classical allegory / vanitas | | real-person | celebrity、明星、politician | fictional character / 80s aesthetic | | brand | marvel、disney、nike | superhero comic style / classic animated | **平台分级**： - DALL-E `max` 严格度 - MJ `high` 中等 - SD/SDXL/Flux `low` 宽松（开源本地） ### 7. Polish + Safety 串联（最强组合） ```bash # Claude 智能润色 → 平台合规重写 → 88 预设增强 ./scripts/enhance_prompt.py "战士在血战之后凝视远方" --polish --safety dalle -j ``` 输出 JSON 包含 `claude_polish` 和 `safety_lint` 两个完整 meta 块，可追溯每一步改写过程。 ## v2.2 新功能详解 ### 1. 混合预设 `-p A+B --mix 0.6` ```bash # 主预设 60% 权重，副预设 40% enhance_prompt.py "持剑女侠" -p "赛博朋克+水墨" --mix 0.6 -m Midjourney enhance_prompt.py "山中神女" -p "原神+敦煌壁画" --mix 0.5 -m SDXL enhance_prompt.py "极简卡片" -p "玻璃拟态+侘寂" --mix 0.7 -m SD ``` 融合策略： - **tags**：主预设标签前置，副预设按权重补充；SD 自动加权重语法 `(tag:1.16)` - **camera**：取主预设（避免镜头语言混乱） - **lighting**：叠加 `主光照, blended with 副光照` - **palette**：拼接两者 - **aspect**：取主预设默认画幅 - **neg**：合并去重 + PRESET_NEG_EXCLUDE 主辅都生效（避免 logo/text/signature 自我否定） - **seed**：mix_label `A+B@0.60` 参与 hash，相同混合每次同 seed ### 2. 视频提示词 `enhance_video.py` ```bash # Sora 8 秒赛博朋克 enhance_video.py "雨夜霓虹街头一只猫漫步" -p 赛博朋克 -m Sora --duration 8 # Kling 慢速跟拍 enhance_video.py "汉服少女转身回眸" -p 汉服写真 -m Kling --motion 慢速跟拍 # 史诗节奏 + 自定义动作 enhance_video.py "宇宙飞船穿越星云" -p scifi -m Runway --pacing 史诗 --action "ship accelerates, lens flare" # 混合风格 + 海螺 MiniMax enhance_video.py "山中神女腾云" -p "原神+敦煌壁画" --mix 0.6 -m Hailuo # 列出所有视频模型规格 enhance_video.py --list-models ``` 支持的视频模型： | 模型 | 上限时长 | 默认画幅 | 提示词风格 | |------|---------|---------|-----------| | Sora | 20s (Sora 2 Pro) | 16:9 | 长自然语言 | | Kling 可灵 | 10s (1080p Pro) | 16:9 | 中文优秀，前置主体 | | Runway Gen-3/4 | 10s | 16:9 | 英文最佳 | | Pika | 10s | 16:9 | 标签式 + `-gs/-motion` | | Luma DreamMachine | 9s | 16:9 | 自然语言 + 关键帧 | | Hailuo MiniMax | 10s | 16:9 | 中英双语 + 参考人物 | | 即梦 Seedance | 12s | 16:9 | 中文多镜头剧情 | | 通义 Wan2.1 | 8s | 16:9 | 阿里开源 14B/1.3B | 输出包含：正向 / 负向（视频专属：flicker、motion blur、identity drift）/ 三段式关键帧 / 一致性六锁（+ motion）。 ### 3. 参考图反解 `reverse_prompt.py` ```bash # 自动识别 A1111/ComfyUI/NovelAI metadata reverse_prompt.py /path/to/image.png # 远程 URL reverse_prompt.py https://example.com/img.png # 直接给 Midjourney 复用 prompt（一行） reverse_prompt.py img.png --mj # 强制 VLM 模板（图无 metadata） reverse_prompt.py img.png --vlm # JSON pipe 给 enhance_prompt.py reverse_prompt.py img.png -j > recipe.json ``` 三层反解： 1. **PNG metadata**：手写 `tEXt`/`iTXt` 解析，零 PIL 依赖 2. **A1111 / ComfyUI / NovelAI 三大格式自动识别** 3. **VLM fallback**：图无 metadata 时输出标准 prompt 给 GPT-4o/Claude/Gemini/Qwen-VL 启发式预设猜测：35+ 关键词映射（cyberpunk → 赛博朋克 / ghibli → 宫崎骏 / dunhuang → 敦煌壁画 ...）。 ### 4. 直出图片 `render_prompt.py` ```bash # Dry-run（只输出 recipe，不出图） render_prompt.py "敦煌神女" -p 敦煌壁画 --backend none -j # AUTOMATIC1111 / Forge SD WebUI render_prompt.py "赛博朋克猫" -p 赛博朋克 --backend sd-webui # ComfyUI（用内置 SDXL workflow） render_prompt.py "原神少女" -p 原神 --backend comfyui # ComfyUI（自定义 workflow） render_prompt.py "原神少女" -p 原神 --backend comfyui --workflow ./workflows/sdxl.json # DALL-E 3 render_prompt.py "极简logo" -p Logo设计 --backend dalle --size 1024x1024 ``` 特点： - **零第三方依赖**：纯 urllib，避免企业扫描器命中 - **环境变量覆盖**：`COMFYUI_URL` / `SDWEBUI_URL` / `OPENAI_API_KEY` - **支持混合预设直出** ## 参考文档 `references/t2i-guide.md` — 提示词要素表 / 88 预设对照 / 模型差异 / 一致性技巧。 ## 版本历史 见 `CHANGELOG.md`。

path: CHANGELOG.md
content: # Changelog

## v2.3.0 — 2026-04-26

**接入 Claude API + 平台合规润色，并起中文别名「火一五文生图提示词」。**

### 中文别名

`displayName: 火一五文生图提示词`，aliases 列表新增`火一五文生图提示词` 排第一位。

### enhance_prompt.py — 加 --polish / --safety

| 参数 | 作用 |
|------|------|
| `--polish` | 先调 Claude API（ANTHROPIC_API_KEY）智能润色，再走 88 预设增强 |
| `--safety <platform>` | 平台合规重写：DALL-E/MJ/SD/SDXL/Flux，自动把可能误判的艺术词替换 |

两者可叠加使用：先 polish 让 Claude 写出专业描述，再 safety 把误判词艺术化。

### 新增脚本：claude_polish.py（350 行）

- **Claude API 直调**：纯 urllib，不引入 anthropic SDK，避免企业扫描器
- **prompt caching 启用**：system prompt 用 `cache_control: ephemeral`，省 90% input token
- **Prefill `{` + JSON 强约束**：assistant 起手 prefill 强制结构化输出
- **88 风格预设嵌入 system prompt**：让 Claude 从清单里挑而非凭记忆
- **XML 思维链**：内部 `<thinking>` 让 Claude 分步骤思考（refine/style/camera/safety/negatives）
- **Platform warnings**：Claude 主动识别 DALL-E/MJ/SD 各自的风险点并给出建议
- **--pipe**：输出可直接喂给 enhance_prompt.py 的 CLI 命令

### 新增脚本：safety_lint.py（330 行）

**仅服务合法艺术创作场景**，不做 jailbreak：

✗ 红线（直接拒答）：
- CSAM（任何含未成年 + 性化的组合）
- 真人 + 色情 / 政治污蔑
- 武器/毒品/爆炸物**制作方法/教程**
- 自残/自杀**方法诱导**

✓ 黄区（艺术化重写）：
- **violence**: 鲜血/血/伤口/kill/murder/weapon/gun/knife → crimson splash / battle-scarred / vanquish / ceremonial blade
- **nudity**: naked/nude/裸/sexy → classical figure study / fine art reference / fashion editorial
- **horror**: horror/scary/gore/monster/demon/evil → gothic atmospheric tension / mythical creature / dark fantasy
- **death**: dead/corpse/skeleton/skull → memento mori / classical allegory / vanitas still life
- **real-person**: celebrity/明星/actor/politician → fictional protagonist / 80s aesthetic
- **brand**: marvel/disney/nike/iphone → superhero comic style / classic animated film / athletic sportswear
- **weapon-model**: ak47/glock/uzi → fictional assault rifle prop

每词内置 `category` + `platforms_affected`。平台分级：
- DALL-E `max` 严格度：所有黄区都触发高风险标记
- MJ `high` 中等：暴力/真人/品牌触发高风险
- SD/SDXL/Flux `low` 宽松（开源）：只对成人内容触发中风险

输出三模式：默认人类可读 / `-j` JSON / `--apply` 直接输出重写文本（pipe 友好）。

### 兼容性

- **完全向下兼容 v2.2**：所有新参数有默认值
- `--polish` 需 `ANTHROPIC_API_KEY`，未设置时报友好错误并不影响其他功能
- `--safety` 是纯本地词典，无网络依赖

### 设计原则

我们**坚决不做** jailbreak / 越狱 / 绕过模型对齐：
- 仅做"合法艺术创作场景下的平台误判规避"
- 红线检测优先于重写
- 替换词全部来自正规艺术、摄影、影视术语
- 用户输入红线内容时直接 `sys.exit(2)` 并给出改写建议

---

## v2.2.0 — 2026-04-25

**四件套大版本：混合预设 + 视频提示词 + 参考图反解 + 直出图片。**

### 新增脚本

| 脚本 | 作用 | 关键参数 |
|------|------|---------|
| `enhance_prompt.py` | 文生图（升级） | `-p A+B --mix 0.6` |
| `enhance_video.py` ⭐ | 视频提示词 | `-m Sora/Kling/Runway/Pika/Luma/Hailuo/即梦/Wan` |
| `reverse_prompt.py` ⭐ | 参考图反解 | A1111 / ComfyUI / NovelAI metadata + VLM 模板 |
| `render_prompt.py` ⭐ | 提示词直出 | `--backend comfyui/sd-webui/dalle/none` |

### enhance_prompt.py — 混合预设

- **`-p "A+B"` 语法**：`赛博朋克+水墨` / `原神+敦煌壁画` / `glassmorphism+wabisabi` 任意两两融合
- **`--mix <ratio>`**：主预设权重 0.1-0.9（默认 0.6）
- **SD 模式**：自动加权重语法 `(primary_tag:1.16), (secondary_tag:1.04)`
- **MJ/Flux/通用**：按比例前置主预设标签
- **camera/lighting/palette 智能融合**：相机沿主预设、光影叠加、色板拼接、aspect 取主
- **PRESET_NEG_EXCLUDE 双向生效**：主辅任一需要 logo/text/signature 都会从 universal_neg 剔除
- **seed 锁定**：mix_label `A+B@0.60` 参与 hash，相同混合每次生成相同 seed

### enhance_video.py — 视频提示词（新文件 470 行）

- **9 大视频模型规格**：Sora / Kling 可灵 / Runway Gen-3/4 / Pika / Luma DreamMachine / Hailuo MiniMax / 即梦 Seedance / 通义 Wan2.1 / 通用
- **30+ 镜头运动词典**：推/拉/摇/移/跟/环绕/手持/航拍/希区柯克/POV/子弹时间/延时/慢动作 ...
- **9 节奏档位**：缓慢 / 宁静 / 中速 / 紧张 / 急促 / 快切 / 动感 / 史诗 ...
- **30+ 主体动作自动抽词**：走/跑/跳/飞/舞/回眸/转身/挥剑/骑马/对视 ...
- **关键帧三段式拆分**：开场建立 → 中段动作峰值 → 结尾落点
- **视频专属负面词**：flicker / motion blur artifacts / identity drift / morphing artifacts
- **复用 88 风格预设 + 混合预设**：视觉锁完全沿用 image preset 体系
- **格式适配**：Pika 输出标签式，其他全部自然语言

### reverse_prompt.py — 参考图反解（新文件 340 行）

- **三层反解策略**：
  1. **PNG metadata**：手写 PNG `tEXt`/`iTXt` 解析，零依赖（不引入 PIL）
  2. **A1111/ComfyUI/NovelAI 三大格式自动识别**：parameters / prompt+workflow / Description+Comment
  3. **VLM fallback 模板**：图无 metadata 时，输出标准化 88 预设选择 prompt 给 GPT-4o/Claude/Gemini/Qwen-VL
- **启发式预设猜测**：35+ 关键词 → 预设映射（cyberpunk → 赛博朋克 / ghibli → 宫崎骏 / dunhuang → 敦煌壁画 ...）
- **画幅自动推断**：从 size 字段算 ratio，匹配最近的 1:1/16:9/3:4/21:9 等
- **三种输出**：`text`(默认) / `--mj`(单行 MJ prompt) / `-j`(结构化 JSON 可 pipe)
- **支持本地路径 + 远程 URL**

### render_prompt.py — 直出图片（新文件 270 行）

- **4 个后端**：
  - `comfyui` — 本地 ComfyUI HTTP API（默认 http://127.0.0.1:8188）
  - `sd-webui` — AUTOMATIC1111 / Forge txt2img API（默认 http://127.0.0.1:7860）
  - `dalle` — OpenAI DALL-E 3（OPENAI_API_KEY）
  - `none` — dry-run，只输出 recipe JSON 不出图
- **零第三方依赖**：纯 urllib，避免企业扫描器命中
- **ComfyUI 默认 workflow**：内置 SDXL 9 节点 workflow，可用 `--workflow` 覆盖
- **环境变量覆盖**：`COMFYUI_URL` / `SDWEBUI_URL`
- **支持混合预设直出**

### 新增功能矩阵

| 维度 | v2.1 | v2.2 |
|------|------|------|
| 出图前 | 提示词增强 | + **混合预设**（任意两两融合） |
| 出图中 | （手工复制到模型） | + **直出**（comfyui/sd-webui/dalle） |
| 出图后 | （无） | + **反解**（A1111/ComfyUI/NovelAI metadata） |
| 视频 | （不支持） | + **视频提示词**（9 模型 + 关键帧 + 镜头运动） |

### 兼容性

- **完全向下兼容**：v2.1 所有 CLI 命令在 v2.2 不变；新参数均有默认值
- **JSON 字段新增**：`mix_secondary` / `mix_ratio` / `mix_label`（旧字段保留）
- **enhance_video.py / reverse_prompt.py / render_prompt.py 是新文件**，不影响 enhance_prompt.py 老用户

### 未变

- 88 风格预设、五锁机制、系列模式、角色设定图、质量档位 — 全部保留

---

## v2.1.0 — 2026-04-24

**再扩充：更贴近需求 + 更多风格 + 角色一致性。**

### 新增风格预设（+32 款，总 56 → 88）

- **游戏艺术（新类，7）**：原神 / 崩铁星穹 / 英雄联盟 / 暗黑4 / Valorant / Pokemon / 暴雪风
- **东方传统（新类，7）**：敦煌壁画 / 青花瓷 / 民国月份牌 / 年画 / 剪纸 / 和风 / 汉服写真
- **动漫扩展（+4）**：萌系 / 厚涂 / 轻小说封面 / 赛璐璐
- **现代设计（+6）**：玻璃拟态 / 新拟态 / 孟菲斯 / 杂志编排 / 包豪斯 / 奶油风
- **建筑氛围（+3）**：粗野主义 / 北欧极简 / 侘寂
- **摄影扩展（+3）**：暗黑美食 / 日杂 / 街头潮流
- **氛围综合（+2）**：疗愈治愈 / 美式复古

### 新功能

- **角色设定图模式** `--character-sheet` / `-cs`：
  - 自动生成 T-pose + 正面 / 三分之二 / 侧面 / 背面多视图的设定图提示词
  - 专为 Midjourney `--cref`、Stable Diffusion IP-Adapter 做角色参考用
  - 画幅自动锁 16:9
- **时间 / 天气 / 季节 自动抽词**：
  - 14 时间词：清晨 / 黎明 / 黄昏 / 日落 / 深夜 / 蓝调时刻 / 魔法时刻 ...
  - 15 天气词：晴天 / 下雨 / 暴雨 / 下雪 / 暴雪 / 雾天 / 雷雨 ...
  - 10 季节词：春夏秋冬 / 樱花季 / 枫叶季
- **负向需求识别**：识别主体描述中的"不要X / 没有X / 避免X / no X / avoid X / without X"，自动从正向提示中移除并加入负面提示。
- **质量档位** `-t basic / pro / master`：
  - `basic`: `high quality, detailed`（省 token）
  - `pro` (默认): `masterpiece, best quality, ultra detailed, 8k`
  - `master`: 叠加 `hdr, intricate details, sharp focus, award winning, trending on artstation, professional, highly polished`
- **显式负面追加** `--avoid "cluttered, people"`：CLI 级附加负面词。

### 新增别名（+45）

`genshin` / `mihoyo` / `honkai` / `starrail` / `lol` / `diablo` / `valorant` / `pokemon` / `blizzard` / `overwatch` / `dunhuang` / `qinghua` / `porcelain` / `yuefenpai` / `wafu` / `hanfu` / `papercut` / `nianhua` / `moe` / `lightnovel` / `lncover` / `celshaded` / `glassmorphism` / `glass` / `neumorphism` / `memphis` / `editorial` / `bauhaus` / `cream` / `korean` / `brutalism` / `brutalist` / `nordic` / `scandinavian` / `wabisabi` / `zen` / `darkfood` / `muji` / `streetwear` / `hypebeast` / `healing` / `cozy` / `americana` ...

### 改进

- 主体描述中的"不要X"子句会先被 `strip_negative_clauses()` 去除再送入正向提示，避免正向污染。
- `print_prompt()` 输出增加 ⭐ 质量档位、👤 角色设定图模式、🕐 时间、☁️ 天气、🍂 季节、🚫 用户负向 六个新字段展示。
- `list_presets()` 按 8 大类分类展示（新增"游戏" / "东方"分组）。

### 兼容性

- **向下兼容**：v2.0 CLI 命令在 v2.1 完全可用，所有新参数均有默认值。
- **JSON 字段新增**：`quality_tier` / `character_sheet` / `time_of_day` / `weather` / `season` / `user_negatives`（旧字段保留）。

---

## v2.0.0 — 2026-04-24

**大版本升级：一致性 + 贴近需求 + 风格扩充。**

### 新增

- **风格预设 17 → 56**，六大分类：
  - 摄影 10（新增：黑白摄影 / 人像摄影 / 时尚大片 / 美食摄影 / 微距摄影 / 航拍摄影 / 街拍纪实）
  - 动漫 6（新增：新海诚 / 宫崎骏 / 美漫 / Q版 / 童话绘本）
  - 插画 7（新增：工笔国画 / 浮世绘 / 线稿）
  - 3D 7（全部新增：3DC4D / 盲盒手办 / 低多边形 / 等距视图 / 粘土 / 毛毡手工 / 纸艺）
  - 设计 10（新增：平面设计 / Logo设计 / 图标设计 / 信息图 / 品牌KV / 专辑封面 / 电影海报 / 表情包）
  - 艺术史 4（全部新增：印象派 / 后印象派 / 新艺术 / 装饰艺术）
  - 场景氛围 12（新增：黑暗奇幻 / Y2K / Vaporwave / 霓虹灯牌 / 概念艺术）
- **一致性四锁**：每个预设内置 `camera` / `lighting` / `palette` / `aspect` 四项独立锁，系列出图风格不再漂移。
- **系列批量模式** `-s N --variations "A,B,C,D"`：共享 seed + 四锁，主体描述差异化，一次生成一整套。
- **意图识别器**：无需指定 `-p`，脚本从"logo/产品/海报/头像/美食/赛博/水墨..."等关键词自动推荐预设 + 画幅。
- **构图/情绪抽词**：主体描述中的"俯拍/特写/航拍/神秘/温馨/史诗..."自动并入提示词。
- **稳定 seed 建议**：基于 `md5(subject + preset)` 生成 32-bit seed，便于复现。
- **英文 / 同义词别名**：60+ 别名（anime、ghibli、cyberpunk、minimal、3d、logo、neon、vapor…）。
- **多模型精细化适配**：
  - Midjourney 输出 `--ar --stylize`，提示 `--cref/--sref`
  - Stable Diffusion 输出权重语法 `(subject:1.2)`，提示采样器/CFG
  - SDXL 输出推荐尺寸（`1216x832` 等）
  - Flux 输出长句自然语言 + guidance 提示
  - DALL-E 3 输出段落式自然语言
- **JSON 输出** `-j`：结构化一致性锁 + 所有参数，便于下游集成。
- **CLI 增强**：`-a/--aspect`、`--mood`、`--composition`、`--seed`、`-l/--list`、`-v/--version`。

### 修复

- 修复 Logo设计 / 图标设计 / 表情包 / 海报 等预设的**全局负面词包含 "logo/text"** 导致的语义自我否定。
- 修复 水墨 / 工笔国画 / 浮世绘 预设中**负面词包含 "signature"** 与画面印章冲突。

### 破坏性变更

- `build_prompt()` 返回 dict 新增 `aspect` / `seed_suggestion` / `consistency_lock` / `hint` / `version` 字段（向下兼容，原有字段保留）。

---

## v1.0.0 — 2026-04-24（初始版本）

- 17 风格预设（写实摄影 / 胶片摄影 / 动漫 / 赛博朋克 / 水彩 / 油画 / 建筑可视化 / 产品设计 / 像素艺术 / 奇幻 / 科幻 / 复古海报 / 水墨 / 蒸汽朋克 / 极简主义 / 电影感 / 国潮）。
- 支持 Midjourney / SD / DALL-E / 通用 四种输出骨架。
- CLI：`subject -p <preset> -m <model> [-l] [-j]`。


path: _meta.json
content: ownerId: kn7byevkn40d6z4p7ghdb097z983tj33
slug: huo15-img-test
version: 2.3.0

path: references/t2i-guide.md
content: # T2I 提示词工程参考（v2.0）

## 一、提示词核心要素

| 要素 | 说明 | 示例 |
|------|------|------|
| **主体** | 画面核心对象 | a cat, a futuristic building, a woman |
| **材质/介质** | 画面的质感 | oil painting, digital art, photography, watercolor |
| **风格** | 艺术风格 | cyberpunk, impressionist, anime, realistic |
| **镜头/构图** | 视角和取景 | close-up, 85mm f/1.4, wide shot, bird's eye view |
| **光线** | 光照方向和类型 | golden hour, neon glow, soft diffused, rim light |
| **色彩** | 色调倾向 | warm tones, teal & orange, monochromatic, pastel |
| **背景** | 环境设定 | busy city street, empty beach, starfield, studio |
| **情绪** | 画面氛围 | melancholic, epic, cozy, mysterious |
| **画质词** | 画质强化 | masterpiece, best quality, ultra detailed, 8k |
| **负面** | 不想出现的 | low quality, blurry, bad anatomy, extra fingers |

## 二、贴近需求的技巧

想让图**贴近你的真实想法**，只给主体不够，建议提供至少以下 3 个维度：

1. **主体 + 动作/状态**：`红发女侠 持剑站立` 比 `红发女侠` 好
2. **环境/背景**：`在雨夜的东京巷弄` 比默认空背景更可控
3. **情绪/时间**：`黄昏，忧郁感` 给画面定调

脚本的 **意图识别器** 会自动抽取其中的构图词（俯拍/特写/远景/航拍…）和情绪词（神秘/温馨/史诗…）并并入提示词。

## 三、一致性的五道防线

系列图"看起来不像同一套"是最常见痛点。按优先级部署：

| # | 机制 | 作用 | 脚本支持 |
|---|------|------|---------|
| 1 | **seed 锁定** | 同 seed + 同提示词 → 几乎复现 | ✅ `--seed` / 自动哈希 |
| 2 | **camera 锁** | 焦段/视角不变 | ✅ 预设内置 |
| 3 | **lighting 锁** | 光源方向、色温不变 | ✅ 预设内置 |
| 4 | **palette 锁** | 色板不变（最影响"同系列感"） | ✅ 预设内置 |
| 5 | **aspect 锁** | 画幅不变 | ✅ 预设内置 |
| 6 | **参考图** | MJ `--cref`/`--sref`、SD IP-Adapter、Flux redux | 提示词输出 |

## 四、56 预设对照表（分类）

### 摄影 · 10
| 预设 | 适用场景 | 画幅 | 核心锁 |
|------|---------|------|--------|
| 写实摄影 | 人像 / 产品 / 建筑 | 3:4 | Canon R5 85mm + 影棚光 |
| 胶片摄影 | 人文 / 旅拍 | 3:2 | 35mm 胶片 + 自然光 |
| 黑白摄影 | 纪实 / 艺术 | 1:1 | Leica M6 + 强对比 |
| 人像摄影 | 肖像 / 头像 | 3:4 | 85mm f/1.4 + 伦勃朗光 |
| 时尚大片 | 时装 / 美妆 | 3:4 | 中画幅 + 硬光 |
| 美食摄影 | 菜品 / 食谱 | 1:1 | 100mm 微距 + 45°侧光 |
| 产品摄影 | 电商白底 | 1:1 | 90mm 微距 + 大柔光箱 |
| 微距摄影 | 昆虫 / 花蕊 | 1:1 | 100mm 1:1 + 环形闪光 |
| 航拍摄影 | 风景 / 城市 | 16:9 | 无人机 24mm 俯视 |
| 街拍纪实 | 人文街头 | 3:2 | 35mm + 环境光 |

### 动漫 · 6
| 预设 | 风格取向 | 画幅 |
|------|---------|------|
| 动漫 | 通用 pixiv 二次元 | 3:4 |
| 新海诚 | 云景 + 辉光 | 16:9 |
| 宫崎骏 | 吉卜力温暖 | 16:9 |
| 美漫 | marvel/DC 粗线条 | 2:3 |
| Q版 | 三头身 chibi | 1:1 |
| 童话绘本 | 水粉儿童绘本 | 4:3 |

### 插画 · 7
| 预设 | 介质/流派 | 画幅 |
|------|----------|------|
| 水彩 | 湿画法纸本 | 1:1 |
| 油画 | 厚涂油彩 | 4:5 |
| 水墨 | 宣纸墨色 | 3:4 |
| 工笔国画 | 矿物颜料工笔 | 3:4 |
| 浮世绘 | 江户时期木版 | 2:3 |
| 线稿 | 纯黑白线条 | 1:1 |
| 像素艺术 | 16-bit sprite | 1:1 |

### 3D · 7
| 预设 | 材质/风格 | 画幅 |
|------|----------|------|
| 3DC4D | Octane 光泽渲染 | 1:1 |
| 盲盒手办 | 泡泡玛特塑胶 | 1:1 |
| 低多边形 | 低面数面片 | 1:1 |
| 等距视图 | 等轴 2.5D | 1:1 |
| 粘土 | 定格动画黏土 | 1:1 |
| 毛毡手工 | 羊毛毡戳制 | 1:1 |
| 纸艺 | 切纸层叠 | 1:1 |

### 设计 · 10
| 预设 | 产出物 | 画幅 |
|------|--------|------|
| 极简主义 | 瑞士派 / 留白 | 1:1 |
| 平面设计 | 矢量插画 | 1:1 |
| Logo设计 | 品牌标志 | 1:1 |
| 图标设计 | app icon | 1:1 |
| 信息图 | 数据可视化 | 3:4 |
| 品牌KV | 广告主视觉 | 16:9 |
| 专辑封面 | 音乐封面 | 1:1 |
| 复古海报 | 1950s letterpress | 3:4 |
| 电影海报 | 院线 one-sheet | 2:3 |
| 表情包 | 贴纸 / emoji | 1:1 |

### 艺术史 · 4
| 预设 | 流派 | 画幅 |
|------|------|------|
| 印象派 | 莫奈 plein-air | 4:5 |
| 后印象派 | 梵高表现 | 4:5 |
| 新艺术 | Mucha 装饰曲线 | 2:3 |
| 装饰艺术 | 盖茨比几何 | 2:3 |

### 场景氛围 · 12
| 预设 | 调性 | 画幅 |
|------|------|------|
| 赛博朋克 | 霓虹 + 雨夜 | 21:9 |
| 蒸汽朋克 | 黄铜维多利亚 | 3:2 |
| 科幻 | 蓝灰硬科幻 | 21:9 |
| 奇幻 | 魔戒史诗 | 16:9 |
| 黑暗奇幻 | 贝尔塞尔克 | 2:3 |
| 国潮 | 朱红鎏金 | 3:4 |
| Y2K | 千禧铬纹 | 1:1 |
| Vaporwave | 蒸汽波落日 | 16:9 |
| 霓虹灯牌 | 玻璃管发光字 | 3:2 |
| 建筑可视化 | V-Ray archviz | 16:9 |
| 电影感 | ARRI + 橙青调色 | 21:9 |
| 概念艺术 | ILM matte | 21:9 |

## 五、模型差异提示

| 模型 | 骨架 | 特有技巧 |
|------|------|----------|
| **Midjourney v6** | 逗号分隔短句 + 尾部 flag | `--cref` 锁角色、`--sref` 锁风格、`--stylize` 控风格化程度、`--chaos` 控多样性 |
| **Stable Diffusion 1.5** | tag 式 + 权重 | `(subject:1.3)`、`[减弱:0.7]`、DPM++ 2M Karras, 30 steps, CFG 6-7 |
| **SDXL** | 同 SD，tag 稍长 | 原生 1024 分辨率、DPM++ SDE Karras, 25-30 steps, CFG 5-7, Refiner 0.2 |
| **DALL-E 3** | 自然语言段落 | ChatGPT 对话中"use the same character" 跨对话续图 |
| **Flux Dev** | 长句，可含短语位置 | guidance 3.5、擅长生成清晰文字、redux 可作参考图 |

## 六、系列一致性工作流（推荐）

### 场景：出一套品牌 4 张产品图

```bash
./scripts/enhance_prompt.py "无线蓝牙耳机 白色" -p 产品摄影 -s 4 \
    --variations "正面特写,45度角展示,充电盒开启,佩戴模特"
```

产出：

1. 所有 4 张输出 **共享 seed**
2. 所有 4 张 **共享 camera / lighting / palette / aspect 锁**
3. 主体描述 **仅在动作/角度上变化**

把这 4 条提示词分别喂给你的生图工具，加上 `--cref <第1张URL>`（MJ）或 IP-Adapter（SD/ComfyUI）即可得到风格完全一致的一套产品图。

### 场景：出一套角色立绘

```bash
./scripts/enhance_prompt.py "银发机甲少女" -p 动漫 -s 6 \
    --variations "正面站立,侧面剪影,奔跑姿势,持武器pose,受伤后,胜利姿态" \
    -m Midjourney
```

记得在 MJ 里给第一张做 `--cref`，后续 5 张引用同一 URL，角色脸部 95%+ 一致。

## 七、Prompt 写作红线

- ❌ 英文提示词里**堆砌中文地名**（除非模型是中文 checkpoint）
- ❌ 一次塞超过 **8 个并列风格**（风格冲突 → 画面混乱）
- ❌ 把**颜色描述**堆得比主体还多（模型会优先表现颜色忽略主体）
- ❌ 负面词**过长**（SD 负面超过 75 tokens 后生效打折）
- ✅ 主体用英文、风格用英文、地域/文化用英文（例 "Chinese traditional garden"）
- ✅ 想要稳定输出：用预设 + seed 锁；想要探索：去 seed / 加 `--chaos 30`


path: scripts/claude_polish.py
content: #!/usr/bin/env python3
"""
huo15-img-test — Claude API 智能润色 v2.3

用 Claude（Anthropic API）把粗糙的中文描述润色成专业 T2I 提示词。
利用 Claude 的 prompt engineering 优势：
  - **结构化思维**：用 XML 标签让 Claude 分步思考（subject_refine → style_pick → camera_lighting → palette → negatives）
  - **prompt caching**：system prompt 缓存，省 90% token
  - **JSON 强约束输出**：用 prefill + tool-use 强制结构化
  - **中英双语理解**：中文输入 → 中英混合输出（视觉术语用英文）

调用：
  claude_polish.py "一个温柔的女孩在花丛中"
  claude_polish.py "赛博朋克猫" --model claude-sonnet-4-5
  claude_polish.py "敦煌神女" --include-safety   # 同时跑 safety_lint
  claude_polish.py "汉服少女" -j > polished.json
  claude_polish.py "雪山下的小屋" --pipe         # 输出可直接喂给 enhance_prompt.py 的 CLI

依赖：
  环境变量 ANTHROPIC_API_KEY
  纯 urllib，零第三方包（不引入 anthropic SDK，避免企业扫描器）
"""

import sys
import os
import json
import argparse
import re
from typing import Dict, List, Optional, Tuple
from urllib.request import Request, urlopen
from urllib.error import HTTPError, URLError

sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))

from enhance_prompt import STYLE_PRESETS

VERSION = "2.3.0"

# ─────────────────────────────────────────────────────────
# Claude API 配置
# ─────────────────────────────────────────────────────────
ANTHROPIC_BASE = os.environ.get("ANTHROPIC_BASE_URL", "https://api.anthropic.com")
ANTHROPIC_VERSION = "2023-06-01"
DEFAULT_MODEL = "claude-sonnet-4-5"  # 用户记忆里偏好的版本


# ─────────────────────────────────────────────────────────
# System Prompt（启用 prompt caching）
# ─────────────────────────────────────────────────────────
def build_system_prompt() -> str:
    """生成 system prompt — 含 88 预设清单。"""
    by_cat: Dict[str, List[str]] = {}
    for name, data in STYLE_PRESETS.items():
        by_cat.setdefault(data["category"], []).append(name)

    preset_block = "\n".join([
        f"- {cat}: " + " / ".join(by_cat[cat])
        for cat in ("摄影", "动漫", "插画", "3D", "设计", "艺术", "场景", "游戏", "东方")
        if cat in by_cat
    ])

    return f"""你是火一五文生图提示词的资深 prompt engineer，专精把中文一句话描述润色成高质量、可直接喂给 Midjourney/SD/SDXL/Flux/DALL-E 的提示词。

# 88 风格预设（必须从这里挑一个）
{preset_block}

# 你的工作流程（用 XML 思维链，但只输出最终 JSON）

<thinking>
1. 解析用户主体：核心人/物/场景，剥离修饰
2. 选风格：从 88 预设挑最贴近的 1 个，可选副预设做混合
3. 推导视觉锁：camera（焦段/视角）/ lighting（光源/光质）/ palette（色板）
4. 自动抽词：构图（特写/俯拍/全身）/ 情绪（温暖/史诗/治愈）/ 时间（黄昏/深夜）/ 天气（雨/雾）/ 季节
5. 平台合规检查：识别可能被 SD/MJ/DALL-E 误判的词，做艺术化替代（仅限合法艺术）
6. 写出 negative prompt：常见 artifact + 主题特定排除项
</thinking>

# 输出 JSON 严格 schema

```json
{{
  "subject_refined_zh": "更具体可视化的中文主体描述（保留意境，加视觉细节）",
  "subject_refined_en": "English version, T2I model 友好",
  "style_preset": "从 88 个里挑 1 个准确名",
  "style_preset_secondary": "可选副预设（混合时用），不需要填空字符串",
  "mix_ratio": 0.6,
  "aspect": "1:1 / 3:4 / 16:9 / 21:9 / 9:16 之一",
  "camera": "镜头/焦段/视角，例：'85mm telephoto, low angle, shallow depth of field'",
  "lighting": "光影描述，例：'golden hour rim light, soft fill, cinematic glow'",
  "palette": "色板，例：'muted earth tones, sage green, warm amber'",
  "composition": "特写/近景/中景/全身/俯拍/仰拍/航拍/侧面/背面 之一或多",
  "mood": "温暖/冷峻/神秘/梦幻/欢快/史诗/治愈/紧张 之一",
  "time_of_day": "清晨/黄昏/日落/深夜/蓝调时刻 等（无则空）",
  "weather": "晴/雨/雾/雪 等（无则空）",
  "season": "春/夏/秋/冬/樱花季/枫叶季（无则空）",
  "key_visual_details": ["3-5 个关键视觉元素，英文，有助于 T2I 复现"],
  "negatives": ["3-5 个应避免的事物，英文"],
  "platform_warnings": [
    {{"platform": "DALL-E|MJ|SD", "risk": "可能被拒的原因", "suggestion": "如何改写"}}
  ],
  "polish_notes": "中文一句话总结：你做了什么润色"
}}
```

# 关键原则

- **不要拒答**：用户给的都是合法艺术创作场景。如果有边界词，做艺术化替代（"血" → "crimson splash"，"裸" → "elegant figure, classical art reference"）。仅在涉及 CSAM/真人色情/违法行为时才返回 `{{"error": "out_of_scope"}}`
- **subject_refined_zh 比 subject_refined_en 更具体**：中文版要补全用户没说但摄影师/画师会自然加的细节（光线、表情、动态）
- **camera/lighting/palette 必须能让另一个画师复现同一张图**：避免"美丽光影"这类废话
- **JSON 之外不要任何文字**

记住：你输出的 JSON 会直接被脚本 parse，**不要**包 ```json``` 代码块，**不要**前缀解释。"""


def call_claude(prompt: str, model: str = DEFAULT_MODEL, max_tokens: int = 2048,
                temperature: float = 0.7) -> Dict:
    """调用 Anthropic Messages API。启用 prompt caching。"""
    [REDACTED_SECRET]"ANTHROPIC_API_KEY")
    if not api_key:
        raise RuntimeError(
            "缺少 ANTHROPIC_API_KEY 环境变量。\n"
            "  • macOS/Linux: export ANTHROPIC_API_KEY=sk-ant-...\n"
            "  • 或在 ~/.zshrc / ~/.bashrc 里写入"
        )

    body = {
        "model": model,
        "max_tokens": max_tokens,
        "temperature": temperature,
        "system": [
            {
                "type": "text",
                "text": build_system_prompt(),
                "cache_control": {"type": "ephemeral"},
            }
        ],
        "messages": [
            {
                "role": "user",
                "content": f"<user_subject>{prompt}</user_subject>\n\n请输出 JSON。",
            },
            {
                "role": "assistant",
                "content": "{",  # prefill 强制 JSON 起手
            },
        ],
    }

    req = Request(
        f"{ANTHROPIC_BASE}/v1/messages",
        data=json.dumps(body).encode("utf-8"),
        headers={
            "Content-Type": "application/json",
            "x-api-key": api_key,
            "anthropic-version": ANTHROPIC_VERSION,
        },
        method="POST",
    )

    try:
        with urlopen(req, timeout=120) as r:
            return json.loads(r.read().decode("utf-8"))
    except HTTPError as e:
        err_body = e.read().decode("utf-8", errors="replace")
        raise RuntimeError(f"Claude API HTTP {e.code}: {err_body}")
    except URLError as e:
        raise RuntimeError(f"Claude API 网络错误: {e}")


def parse_claude_json(resp: Dict) -> Dict:
    """从 Claude 响应中抽出 JSON（已 prefill `{`，所以拼回去）。"""
    if "error" in resp:
        raise RuntimeError(f"Claude API 错误: {resp['error']}")
    text = ""
    for block in resp.get("content", []):
        if block.get("type") == "text":
            text += block.get("text", "")
    if not text:
        raise RuntimeError(f"Claude 返回空内容: {resp}")

    full = "{" + text  # prefill
    # 截到第一个完整 JSON
    depth = 0
    end = -1
    in_str = False
    esc = False
    for i, ch in enumerate(full):
        if esc:
            esc = False
            continue
        if ch == "\\":
            esc = True
            continue
        if ch == '"':
            in_str = not in_str
            continue
        if in_str:
            continue
        if ch == "{":
            depth += 1
        elif ch == "}":
            depth -= 1
            if depth == 0:
                end = i + 1
                break
    if end == -1:
        raise RuntimeError(f"未找到完整 JSON: {full[:300]}")

    try:
        data = json.loads(full[:end])
    except json.JSONDecodeError as e:
        raise RuntimeError(f"JSON 解析失败: {e}\n原文: {full[:300]}")

    # 附加 usage 信息
    usage = resp.get("usage", {})
    data["_usage"] = {
        "input_tokens": usage.get("input_tokens", 0),
        "output_tokens": usage.get("output_tokens", 0),
        "cache_creation_input_tokens": usage.get("cache_creation_input_tokens", 0),
        "cache_read_input_tokens": usage.get("cache_read_input_tokens", 0),
    }
    data["_model"] = resp.get("model", "")
    return data


# ─────────────────────────────────────────────────────────
# 输出格式化
# ─────────────────────────────────────────────────────────
def to_pipe_command(polished: Dict) -> str:
    """把 polished 转成可直接喂给 enhance_prompt.py 的 CLI 命令。"""
    subject = polished.get("subject_refined_zh", "")
    preset = polished.get("style_preset", "")
    sec = polished.get("style_preset_secondary", "")
    mix = polished.get("mix_ratio", 0.6)
    aspect = polished.get("aspect", "")

    preset_arg = f'"{preset}+{sec}"' if sec else f'"{preset}"'
    parts = [
        "enhance_prompt.py",
        f'"{subject}"',
        "-p", preset_arg,
    ]
    if sec:
        parts += ["--mix", str(mix)]
    if aspect:
        parts += ["-a", aspect]
    return " ".join(parts)


def print_polished(polished: Dict):
    sep = "═" * 60
    print(f"\n{sep}")
    print(f"✨ Claude 智能润色 v{VERSION}")
    print(f"🤖 模型: {polished.get('_model', '?')}")
    u = polished.get("_usage", {})
    print(f"📊 [REDACTED_SECRET]'input_tokens',0)} / out={u.get('output_tokens',0)} / cache_read={u.get('cache_read_input_tokens',0)} (省 token)")

    if polished.get("error"):
        print(f"\n❌ 拒答: {polished['error']}（CSAM/真人色情/违法 不在本工具支持范围）")
        print(f"{sep}\n")
        return

    print(f"\n📝 润色后中文主体:\n   {polished.get('subject_refined_zh', '')}")
    print(f"\n🌐 English:\n   {polished.get('subject_refined_en', '')}")

    style = polished.get("style_preset", "")
    sec = polished.get("style_preset_secondary", "")
    if sec:
        ratio = polished.get("mix_ratio", 0.6)
        print(f"\n🎨 推荐预设: {style} + {sec} (mix={ratio})")
    else:
        print(f"\n🎨 推荐预设: {style}")

    print(f"📐 画幅: {polished.get('aspect', '')}")
    print(f"🎥 相机: {polished.get('camera', '')}")
    print(f"💡 光影: {polished.get('lighting', '')}")
    print(f"🎨 色板: {polished.get('palette', '')}")

    extras = []
    for k, label in [("composition", "构图"), ("mood", "情绪"),
                     ("time_of_day", "时间"), ("weather", "天气"), ("season", "季节")]:
        if polished.get(k):
            extras.append(f"{label}={polished[k]}")
    if extras:
        print(f"🔍 抽词: {' / '.join(extras)}")

    if polished.get("key_visual_details"):
        print(f"\n🌟 关键视觉:")
        for d in polished["key_visual_details"]:
            print(f"   • {d}")

    if polished.get("negatives"):
        print(f"\n🚫 负面词:")
        for n in polished["negatives"]:
            print(f"   • {n}")

    warnings = polished.get("platform_warnings") or []
    if warnings:
        print(f"\n⚠️  平台风险:")
        for w in warnings:
            print(f"   [{w.get('platform','?')}] {w.get('risk','')}")
            print(f"      → {w.get('suggestion','')}")

    if polished.get("polish_notes"):
        print(f"\n📌 润色说明: {polished['polish_notes']}")

    print(f"\n💡 一键复制喂给 enhance_prompt.py:")
    print(f"   {to_pipe_command(polished)}")
    print(f"{sep}\n")


def main():
    parser = argparse.ArgumentParser(
        description=f"huo15-img-test claude_polish v{VERSION} — Claude API 智能润色",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
示例:
  claude_polish.py "一个温柔的女孩在花丛中"
  claude_polish.py "赛博朋克猫" --model claude-sonnet-4-6
  claude_polish.py "敦煌神女" -j > polished.json
  claude_polish.py "雪山下的小屋" --pipe   # 输出可直接喂给 enhance_prompt.py 的命令

环境变量:
  ANTHROPIC_API_KEY     必填
  ANTHROPIC_BASE_URL    可选，默认 https://api.anthropic.com
""",
    )
    parser.add_argument("subject", nargs="?", help="主体描述（中文/英文均可）")
    parser.add_argument("--model", default=DEFAULT_MODEL,
                        help=f"Claude 模型（默认 {DEFAULT_MODEL}）")
    parser.add_argument("--max-tokens", type=int, default=2048, help="最大输出 tokens")
    parser.add_argument("--temperature", type=float, default=0.7, help="温度 0.0-1.0")
    parser.add_argument("--pipe", action="store_true", help="输出 enhance_prompt.py CLI 命令一行")
    parser.add_argument("-j", "--json", action="store_true", help="JSON 输出")
    parser.add_argument("-v", "--version", action="version", version=f"%(prog)s v{VERSION}")
    args = parser.parse_args()

    if not args.subject:
        parser.print_help()
        sys.exit(1)

    try:
        resp = call_claude(args.subject, model=args.model,
                           max_tokens=args.max_tokens, temperature=args.temperature)
        polished = parse_claude_json(resp)
    except RuntimeError as e:
        print(f"❌ {e}", file=sys.stderr)
        sys.exit(2)

    if args.pipe:
        print(to_pipe_command(polished))
        return
    if args.json:
        print(json.dumps(polished, ensure_ascii=False, indent=2))
        return
    print_polished(polished)


if __name__ == "__main__":
    main()


path: scripts/enhance_prompt.py
content: #!/usr/bin/env python3
"""
huo15-img-test — T2I 提示词增强脚本 v2.2

核心能力：
1. 88 风格预设（摄影 / 动漫 / 插画 / 3D / 设计 / 艺术 / 场景 / 游戏 / 东方传统 九大类）
2. 意图解析（主体类型 / 画幅 / 构图 / 情绪 / 时间 / 天气 / 季节）
3. 一致性五锁（camera + lighting + palette + aspect + seed）
4. 系列批量模式（-s N：共享锁，差异化动作）
5. 角色设定图模式（--character-sheet：T-pose 多视图，喂给 MJ --cref）
6. 质量档位（-t basic / pro / master）
7. 负向需求识别（"不要 X" / "no X" / "avoid X" 自动入负面）
8. 多模型精细化适配（Midjourney / SD / SDXL / Flux / DALL-E 3）
9. 别名 & 中英混输入（anime / cyberpunk / 原神 / 敦煌 均可）
10. 混合预设 v2.2：`-p A+B --mix 0.6` 加权融合两套风格（赛博+水墨 / 原神+敦煌 ...）
"""

import sys
import os
import json
import re
import argparse
import hashlib
from typing import Dict, List, Optional, Tuple

VERSION = "2.3.0"

# ─────────────────────────────────────────────────────────
# 通用质量 / 负面词
# ─────────────────────────────────────────────────────────
UNIVERSAL_QUALITY = "masterpiece, best quality, ultra detailed, 8k"
UNIVERSAL_NEG = (
    "low quality, worst quality, lowres, blurry, jpeg artifacts, "
    "watermark, signature, text, logo, username, "
    "bad anatomy, bad hands, extra fingers, missing fingers, "
    "extra limbs, deformed, mutated, disfigured, ugly, "
    "out of frame, cropped, duplicate"
)

# 这些预设天然需要 logo / text / signature，把它们从全局负面词中剔除，避免语义冲突
PRESET_NEG_EXCLUDE: Dict[str, List[str]] = {
    "Logo设计": ["logo", "text"],
    "图标设计": ["logo", "text"],
    "表情包": ["text"],
    "复古海报": ["text"],
    "电影海报": ["text"],
    "专辑封面": ["text"],
    "品牌KV": ["text"],
    "信息图": ["text"],
    "水墨": ["signature"],
    "工笔国画": ["signature"],
    "浮世绘": ["text", "signature"],
    "霓虹灯牌": ["text"],
}


def _filter_neg(universal: str, exclude: List[str]) -> str:
    if not exclude:
        return universal
    tokens = [t.strip() for t in universal.split(",")]
    kept = [t for t in tokens if t.lower() not in {e.lower() for e in exclude}]
    return ", ".join(kept)

# ─────────────────────────────────────────────────────────
# 风格预设 — 每个预设 7 个字段
#   tags     风格标签
#   quality  画质标签
#   neg      负面标签（与 UNIVERSAL_NEG 合并）
#   camera   机位 / 镜头（摄影专用，其它留空）
#   lighting 光影锁
#   palette  色板锁（系列一致性关键）
#   aspect   默认画幅
# ─────────────────────────────────────────────────────────

STYLE_PRESETS: Dict[str, Dict[str, str]] = {
    # ========== 摄影 Photography ==========
    "写实摄影": {
        "category": "摄影",
        "tags": "photorealistic, hyperrealistic, dslr photography, sharp focus",
        "quality": "raw photo, detailed skin texture, film grain subtle",
        "neg": "cartoon, anime, painting, drawing, illustration, cgi",
        "camera": "Canon EOS R5, 85mm f/1.4 lens, shallow depth of field",
        "lighting": "professional studio lighting, softbox key light, rim light",
        "palette": "natural color grading, balanced exposure",
        "aspect": "3:4",
    },
    "胶片摄影": {
        "category": "摄影",
        "tags": "analog film photography, film grain, analog aesthetic",
        "quality": "kodak portra 400 film stock, scanned film",
        "neg": "digital, oversaturated, hdr, plastic skin",
        "camera": "35mm film camera, 50mm prime, shot on film",
        "lighting": "natural window light, golden hour",
        "palette": "muted earth tones, slightly faded film colors",
        "aspect": "3:2",
    },
    "黑白摄影": {
        "category": "摄影",
        "tags": "black and white photography, monochrome, high contrast",
        "quality": "silver gelatin print, fine art photography, rich grayscale",
        "neg": "color, colorful, saturated, low contrast",
        "camera": "Leica M6, 35mm f/2, classic reportage framing",
        "lighting": "dramatic chiaroscuro, strong directional light",
        "palette": "pure black and white, deep blacks, crisp whites",
        "aspect": "1:1",
    },
    "人像摄影": {
        "category": "摄影",
        "tags": "portrait photography, shallow depth of field, bokeh background",
        "quality": "flawless skin retouch, detailed eyes, catch light",
        "neg": "full body, wide shot, plastic skin, uncanny",
        "camera": "85mm f/1.4, eye-level portrait, rule of thirds",
        "lighting": "rembrandt lighting, soft key with fill",
        "palette": "warm skin tones, complementary backdrop",
        "aspect": "3:4",
    },
    "时尚大片": {
        "category": "摄影",
        "tags": "high fashion editorial, vogue style, avant-garde styling",
        "quality": "magazine cover quality, haute couture",
        "neg": "amateur, casual, snapshot, cluttered set",
        "camera": "medium format, 50mm, full body or waist-up",
        "lighting": "hard strobe with deep shadows, beauty dish",
        "palette": "high-contrast, bold monochromatic set",
        "aspect": "3:4",
    },
    "美食摄影": {
        "category": "摄影",
        "tags": "food photography, overhead flatlay, appetizing presentation",
        "quality": "detailed steam and texture, drool-worthy, michelin plating",
        "neg": "greasy, unappealing, blurry plate, messy",
        "camera": "macro 100mm, 45-degree angle or top-down",
        "lighting": "soft window light from side, subtle rim highlight",
        "palette": "warm appetite-triggering tones, natural food colors",
        "aspect": "1:1",
    },
    "产品摄影": {
        "category": "摄影",
        "tags": "commercial product photography, clean composition, minimal scene",
        "quality": "crisp reflections, seamless background, advertising grade",
        "neg": "cluttered, messy background, amateur lighting",
        "camera": "90mm macro, eye-level product shot",
        "lighting": "large softbox key, gradient sweep background",
        "palette": "neutral white or brand-matched seamless",
        "aspect": "1:1",
    },
    "微距摄影": {
        "category": "摄影",
        "tags": "macro photography, extreme close-up, micro world",
        "quality": "razor sharp details at micro scale, focus stacking",
        "neg": "soft focus, wide view, lack of detail",
        "camera": "100mm macro lens, 1:1 magnification",
        "lighting": "ring flash or twin macro flash, even diffused",
        "palette": "nature-true colors, intense saturation",
        "aspect": "1:1",
    },
    "航拍摄影": {
        "category": "摄影",
        "tags": "aerial photography, drone shot, bird's eye view",
        "quality": "ultra wide sweeping vista, high altitude clarity",
        "neg": "ground level, close-up, people center frame",
        "camera": "drone camera, 24mm equivalent, top-down or 45-degree",
        "lighting": "natural sunlight, long soft shadows",
        "palette": "earth tones with atmospheric blue haze",
        "aspect": "16:9",
    },
    "街拍纪实": {
        "category": "摄影",
        "tags": "street photography, decisive moment, candid documentary",
        "quality": "authentic raw feeling, unposed human story",
        "neg": "staged, fake, overprocessed",
        "camera": "35mm prime, hip-level snap, off-center subject",
        "lighting": "available ambient light, urban neon or sunlight",
        "palette": "slightly desaturated urban tones",
        "aspect": "3:2",
    },

    # ========== 动漫 / 插画 Illustration ==========
    "动漫": {
        "category": "动漫",
        "tags": "anime style, cel shading, clean line art, vibrant anime colors",
        "quality": "detailed anime eyes, pixiv trending, high quality anime",
        "neg": "photorealistic, 3d render, western cartoon, low quality",
        "camera": "",
        "lighting": "anime-style soft light, rim light on hair",
        "palette": "vibrant saturated anime palette",
        "aspect": "3:4",
    },
    "新海诚": {
        "category": "动漫",
        "tags": "Makoto Shinkai style, volumetric cloudscape, realistic anime backgrounds",
        "quality": "your name aesthetic, weathering with you mood, incredibly detailed skyscape",
        "neg": "flat background, dark mood, gritty",
        "camera": "",
        "lighting": "magic hour sunlight streaming, god rays through clouds",
        "palette": "sky blue, warm orange sunset, pink hour",
        "aspect": "16:9",
    },
    "宫崎骏": {
        "category": "动漫",
        "tags": "Studio Ghibli style, hand-painted background, whimsical warmth",
        "quality": "Totoro aesthetic, Spirited Away mood, hayao miyazaki inspired",
        "neg": "dark, edgy, hyperdetailed, cgi",
        "camera": "",
        "lighting": "soft daylight through leaves, gentle diffuse",
        "palette": "pastoral greens, cream, sky blue",
        "aspect": "16:9",
    },
    "美漫": {
        "category": "动漫",
        "tags": "American comic book style, bold ink lines, halftone shading",
        "quality": "marvel / DC inspired, dynamic pose, action panel",
        "neg": "anime, soft shading, watercolor",
        "camera": "",
        "lighting": "dramatic cel lighting, high contrast shadows",
        "palette": "saturated primary colors, comic book palette",
        "aspect": "2:3",
    },
    "Q版": {
        "category": "动漫",
        "tags": "chibi style, super-deformed, cute mascot, 3-head-tall proportions",
        "quality": "adorable, clean vector look, sticker worthy",
        "neg": "realistic proportion, detailed anatomy, dark mood",
        "camera": "",
        "lighting": "even flat light, gentle cel shading",
        "palette": "bright pastel palette, sugary",
        "aspect": "1:1",
    },
    "童话绘本": {
        "category": "动漫",
        "tags": "children's book illustration, storybook style, hand drawn warmth",
        "quality": "gouache texture, paper warmth, beatrix potter meets pixar",
        "neg": "dark, horror, hyper-realistic, edgy",
        "camera": "",
        "lighting": "soft overall illumination, enchanted glow",
        "palette": "warm buttery pastels, cream page base",
        "aspect": "4:3",
    },
    "水彩": {
        "category": "插画",
        "tags": "watercolor painting, wet-on-wet technique, paper texture, soft bleeding edges",
        "quality": "traditional watercolor, transparent wash layers, artistic",
        "neg": "digital vector, hard edges, heavy outlines, 3d",
        "camera": "",
        "lighting": "natural daylight on paper",
        "palette": "translucent pastel layers, white paper showing through",
        "aspect": "1:1",
    },
    "油画": {
        "category": "插画",
        "tags": "oil painting, thick impasto brushstrokes, canvas texture",
        "quality": "museum quality oil on canvas, old master technique",
        "neg": "digital, flat colors, vector, pixel art",
        "camera": "",
        "lighting": "chiaroscuro, warm rembrandt glow",
        "palette": "rich earth tones, deep jewel colors",
        "aspect": "4:5",
    },
    "水墨": {
        "category": "插画",
        "tags": "Chinese ink wash painting, sumi-e, negative space, calligraphic strokes",
        "quality": "zen atmosphere, rice paper texture, ink bleed",
        "neg": "colorful, dense composition, western painting, cartoon",
        "camera": "",
        "lighting": "flat paper light, no harsh shadows",
        "palette": "sumi black on rice-paper beige, occasional vermillion seal",
        "aspect": "3:4",
    },
    "工笔国画": {
        "category": "插画",
        "tags": "Chinese gongbi painting, meticulous fine brush, intricate floral detail",
        "quality": "Song dynasty court painting style, mineral pigment",
        "neg": "loose brushstrokes, abstract, western style",
        "camera": "",
        "lighting": "flat even pigment, no modeling light",
        "palette": "azurite blue, malachite green, cinnabar red, gold leaf",
        "aspect": "3:4",
    },
    "浮世绘": {
        "category": "插画",
        "tags": "Ukiyo-e woodblock print, Edo period, Hokusai / Hiroshige style",
        "quality": "traditional Japanese woodcut, flat color blocks, outlined figures",
        "neg": "modern anime, 3d, photorealistic",
        "camera": "",
        "lighting": "no modeling light, flat graphic",
        "palette": "prussian blue, earth reds, muted greens",
        "aspect": "2:3",
    },
    "线稿": {
        "category": "插画",
        "tags": "clean line art, black ink on white, single weight or dynamic line",
        "quality": "architectural line drawing precision, tattoo flash clarity",
        "neg": "color, shading, painterly, texture",
        "camera": "",
        "lighting": "no lighting, pure linework",
        "palette": "pure black on white",
        "aspect": "1:1",
    },
    "像素艺术": {
        "category": "插画",
        "tags": "pixel art, 16-bit sprite, pixelated, retro game aesthetic",
        "quality": "clean pixel clusters, limited palette, dithering",
        "neg": "anti-aliased, smooth, photorealistic, 3d render, high resolution",
        "camera": "",
        "lighting": "flat pixel shading or 2-tone",
        "palette": "NES / SNES limited palette, 16 colors",
        "aspect": "1:1",
    },

    # ========== 3D / 手工 3D & Craft ==========
    "3DC4D": {
        "category": "3D",
        "tags": "3d render, octane render, c4d style, subsurface scattering, glossy materials",
        "quality": "ray traced reflections, detailed shader, behance trending",
        "neg": "2d flat, sketch, line art",
        "camera": "3d viewport camera, 50mm equivalent",
        "lighting": "hdri environment light, colored accent rims",
        "palette": "vibrant candy colors, pastel gradients",
        "aspect": "1:1",
    },
    "盲盒手办": {
        "category": "3D",
        "tags": "blind box figurine, pop mart style, chibi 3d toy, kawaii collectible",
        "quality": "vinyl toy finish, pristine product shot, pop mart aesthetic",
        "neg": "realistic human, gritty, damaged",
        "camera": "50mm product shot, eye level toy perspective",
        "lighting": "soft studio light, gentle rim, clean shadow",
        "palette": "pastel macaron palette",
        "aspect": "1:1",
    },
    "低多边形": {
        "category": "3D",
        "tags": "low poly 3d, faceted geometry, minimalist polygons",
        "quality": "crisp flat shaded polygons, geometric stylization",
        "neg": "high detail, smooth subdivisions, realistic",
        "camera": "3/4 perspective, isometric-ish",
        "lighting": "flat faceted shading, 2-3 light setup",
        "palette": "limited flat palette, often pastel",
        "aspect": "1:1",
    },
    "等距视图": {
        "category": "3D",
        "tags": "isometric illustration, 2.5d isometric scene, game-dev isometric tile",
        "quality": "clean vector isometric look, detailed miniature diorama",
        "neg": "perspective distortion, top-down, first-person",
        "camera": "true isometric projection, 30-degree angles",
        "lighting": "even diffuse light, directional accent",
        "palette": "bright clean pastel palette",
        "aspect": "1:1",
    },
    "粘土": {
        "category": "3D",
        "tags": "claymation style, stop motion clay, aardman-like tactile figures",
        "quality": "handmade clay texture, fingerprint detail",
        "neg": "clean digital, plastic, smooth 3d",
        "camera": "stop motion rig perspective, slight depth of field",
        "lighting": "warm tungsten key with practical fill",
        "palette": "warm terracotta tones",
        "aspect": "1:1",
    },
    "毛毡手工": {
        "category": "3D",
        "tags": "felted wool craft, needle felt texture, handmade plush character",
        "quality": "fuzzy fiber detail, cute handmade imperfection",
        "neg": "smooth digital render, photorealistic animal",
        "camera": "close macro product shot",
        "lighting": "soft diffuse daylight",
        "palette": "muted natural wool colors",
        "aspect": "1:1",
    },
    "纸艺": {
        "category": "3D",
        "tags": "paper craft, layered paper art, quilling, origami composition",
        "quality": "intricate cut paper layers, shadow depth between layers",
        "neg": "flat 2d, digital illustration",
        "camera": "front-on with slight tilt, shallow depth",
        "lighting": "rim light casting paper-edge shadows",
        "palette": "pastel construction paper colors",
        "aspect": "1:1",
    },

    # ========== 设计 Design ==========
    "极简主义": {
        "category": "设计",
        "tags": "minimalist design, negative space, swiss style, geometric composition",
        "quality": "clean typography-friendly, editorial layout",
        "neg": "cluttered, ornate, busy, excess detail",
        "camera": "",
        "lighting": "flat studio light or ambient, no drama",
        "palette": "monochrome + single accent, lots of white",
        "aspect": "1:1",
    },
    "平面设计": {
        "category": "设计",
        "tags": "flat design, vector graphic, bold shapes, brand illustration",
        "quality": "clean vectors, designer grade composition",
        "neg": "photorealistic, gradient 3d, sketchy",
        "camera": "",
        "lighting": "flat shading, no highlights",
        "palette": "brand-forward 3-color palette",
        "aspect": "1:1",
    },
    "Logo设计": {
        "category": "设计",
        "tags": "logo design, brand mark, vector logotype, scalable emblem",
        "quality": "professional logo, centered composition on clean background",
        "neg": "photorealistic scene, complex background, cluttered",
        "camera": "",
        "lighting": "flat vector, no light gradient",
        "palette": "2-color max, high contrast",
        "aspect": "1:1",
    },
    "图标设计": {
        "category": "设计",
        "tags": "icon design, app icon, rounded square, centered glyph",
        "quality": "apple hig compliant, clean icon grid, crisp at 1024px",
        "neg": "cluttered, off-center, photo, low contrast",
        "camera": "",
        "lighting": "subtle highlight gradient, soft inner glow",
        "palette": "vibrant gradient with 2-3 colors",
        "aspect": "1:1",
    },
    "信息图": {
        "category": "设计",
        "tags": "infographic design, data visualization, icon system, explanatory layout",
        "quality": "clean editorial infographic, behance level",
        "neg": "messy, illustrative painting, photograph",
        "camera": "",
        "lighting": "flat, no drama",
        "palette": "brand palette + grayscale structure",
        "aspect": "3:4",
    },
    "品牌KV": {
        "category": "设计",
        "tags": "brand key visual, advertising campaign hero image, marketing KV",
        "quality": "commercial campaign quality, headline-ready negative space",
        "neg": "casual, amateur, low contrast",
        "camera": "hero wide or 3/4 product hero",
        "lighting": "brand-defined dramatic key, colored rim",
        "palette": "brand palette dominant + accent",
        "aspect": "16:9",
    },
    "专辑封面": {
        "category": "设计",
        "tags": "album cover art, music artwork, square format composition",
        "quality": "iconic album design, strong concept, emotive",
        "neg": "cluttered, literal, stock imagery",
        "camera": "",
        "lighting": "concept-driven, mood-heavy",
        "palette": "2-3 color highly intentional palette",
        "aspect": "1:1",
    },
    "复古海报": {
        "category": "设计",
        "tags": "vintage poster design, 1950s retro, letterpress print, screenprint texture",
        "quality": "saul bass meets mid-century, weathered paper feel",
        "neg": "modern flat design, digital gradient, 3d render",
        "camera": "",
        "lighting": "flat two-tone",
        "palette": "muted primary + cream background",
        "aspect": "3:4",
    },
    "电影海报": {
        "category": "设计",
        "tags": "movie poster, cinematic key art, title-ready composition",
        "quality": "theatrical one-sheet, dramatic hero composition",
        "neg": "casual snapshot, cluttered, amateur",
        "camera": "hero portrait or symmetric icon layout",
        "lighting": "strong single direction light, volumetric",
        "palette": "teal & orange or moody duotone",
        "aspect": "2:3",
    },
    "表情包": {
        "category": "设计",
        "tags": "sticker design, emoji style, expressive meme-ready character",
        "quality": "transparent background ready, bold outline, readable at 128px",
        "neg": "complex scene, photorealistic, subtle",
        "camera": "",
        "lighting": "flat cel shading",
        "palette": "bright saturated 4-color",
        "aspect": "1:1",
    },

    # ========== 艺术史 Art Movement ==========
    "印象派": {
        "category": "艺术",
        "tags": "impressionist painting, visible brushstrokes, plein air, monet inspired",
        "quality": "late 19th century impressionism, atmospheric perspective",
        "neg": "photorealistic, digital, sharp outlines",
        "camera": "",
        "lighting": "dappled natural light, sun-drenched scene",
        "palette": "broken color technique, complementary dabs",
        "aspect": "4:5",
    },
    "后印象派": {
        "category": "艺术",
        "tags": "post-impressionist, van gogh style, expressive brushstroke, emotive color",
        "quality": "starry night swirls, dynamic brush texture",
        "neg": "realistic, photographic, flat",
        "camera": "",
        "lighting": "emotional not physical light",
        "palette": "bold yellows cobalt and burnt sienna",
        "aspect": "4:5",
    },
    "新艺术": {
        "category": "艺术",
        "tags": "art nouveau, alphonse mucha, flowing organic lines, floral ornament border",
        "quality": "belle époque poster, feminine ornate frame",
        "neg": "geometric minimal, modern flat, 3d",
        "camera": "",
        "lighting": "flat even decorative light",
        "palette": "muted golds, soft earth tones, sage",
        "aspect": "2:3",
    },
    "装饰艺术": {
        "category": "艺术",
        "tags": "art deco, 1920s geometric ornament, gatsby aesthetic, gold and black lacquer",
        "quality": "symmetric art deco pattern, streamline moderne elegance",
        "neg": "rustic, organic nouveau, grunge",
        "camera": "",
        "lighting": "strong geometric shadow play",
        "palette": "black gold ivory with emerald accents",
        "aspect": "2:3",
    },

    # ========== 场景 / 氛围 Scene ==========
    "赛博朋克": {
        "category": "场景",
        "tags": "cyberpunk, neon-soaked, blade runner aesthetic, megacity dystopia, holographic ads",
        "quality": "detailed cyberpunk cityscape, rainy night ambiance",
        "neg": "rustic, medieval, natural countryside",
        "camera": "low angle wide, 24mm anamorphic",
        "lighting": "neon magenta and cyan rim, wet reflective streets",
        "palette": "magenta cyan black, neon highlights",
        "aspect": "21:9",
    },
    "蒸汽朋克": {
        "category": "场景",
        "tags": "steampunk, brass gears and copper pipes, victorian industrial, airship era",
        "quality": "intricate clockwork detail, rich leather and patina",
        "neg": "clean sci-fi, modern, plastic",
        "camera": "",
        "lighting": "warm gaslight glow, smoky haze",
        "palette": "brass copper sepia, burgundy leather",
        "aspect": "3:2",
    },
    "科幻": {
        "category": "场景",
        "tags": "sci-fi concept art, futuristic technology, clean spaceship interior, holographic UI",
        "quality": "blade runner 2049 palette, hard-sci-fi plausible",
        "neg": "medieval, fantasy magic, primitive",
        "camera": "cinematic wide, 21:9 framing",
        "lighting": "cool blue practical strips, volumetric haze",
        "palette": "cool blue cyan with warm accent",
        "aspect": "21:9",
    },
    "奇幻": {
        "category": "场景",
        "tags": "epic fantasy art, magical atmosphere, artstation trending, tolkien inspired",
        "quality": "detailed fantasy concept, elven architecture, dragon-scale atmosphere",
        "neg": "modern city, cyberpunk, mundane",
        "camera": "epic wide establishing, 24mm",
        "lighting": "ethereal god rays through mist",
        "palette": "golden hour warm with magical cyan glow",
        "aspect": "16:9",
    },
    "黑暗奇幻": {
        "category": "场景",
        "tags": "dark fantasy, grimdark, eldritch horror atmosphere, berserk aesthetic",
        "quality": "frank frazetta meets zdzisław beksiński",
        "neg": "cheerful, bright, cartoonish",
        "camera": "low angle hero or dread pov",
        "lighting": "blood moon crimson, torch flicker",
        "palette": "black crimson sickly green, rusted iron",
        "aspect": "2:3",
    },
    "国潮": {
        "category": "场景",
        "tags": "guochao Chinese neo-trend, modern hanfu revival, oriental modernism",
        "quality": "contemporary Chinese style illustration, editorial fashion",
        "neg": "western medieval, european style",
        "camera": "",
        "lighting": "warm accent on oriental red-gold",
        "palette": "vermillion jade gold, ink black accents",
        "aspect": "3:4",
    },
    "Y2K": {
        "category": "场景",
        "tags": "Y2K aesthetic, early 2000s digital, chrome bubble UI, frosted plastic",
        "quality": "low-fi cd-rom graphic, holographic stickers",
        "neg": "ultra clean modern, analog retro",
        "camera": "",
        "lighting": "glossy chrome highlights",
        "palette": "baby blue pink lilac, iridescent chrome",
        "aspect": "1:1",
    },
    "Vaporwave": {
        "category": "场景",
        "tags": "vaporwave, retro 80s 90s computer graphics, roman bust, palm tree grid",
        "quality": "synthwave aesthetic, low-fi jpeg nostalgia",
        "neg": "modern clean, natural, high detail",
        "camera": "",
        "lighting": "sunset gradient, neon grid horizon",
        "palette": "hot pink teal purple, retro sunset",
        "aspect": "16:9",
    },
    "霓虹灯牌": {
        "category": "场景",
        "tags": "neon sign typography, glowing tube letters, dark brick wall backdrop",
        "quality": "realistic neon glass tube glow, chromatic bloom",
        "neg": "daylight, printed sign, flat vector",
        "camera": "straight-on product shot, 50mm",
        "lighting": "self-emissive neon, dark ambient",
        "palette": "magenta cyan on deep black",
        "aspect": "3:2",
    },
    "建筑可视化": {
        "category": "场景",
        "tags": "architectural visualization, V-Ray / Lumion render, interior design magazine",
        "quality": "award-winning archviz, photorealistic materials",
        "neg": "sketchy, doodle, distorted perspective",
        "camera": "wide 24mm architectural tilt-corrected",
        "lighting": "realistic sun study plus artificial, product-ready",
        "palette": "natural materials, neutral brand-defined",
        "aspect": "16:9",
    },
    "电影感": {
        "category": "场景",
        "tags": "cinematic film still, anamorphic lens flare, letterboxed framing",
        "quality": "ARRI Alexa quality, professional color grade, movie still",
        "neg": "snapshot, amateur, flat lighting, instagram filter",
        "camera": "anamorphic 2.39:1 framing, low angle hero",
        "lighting": "motivated practical + volumetric haze",
        "palette": "teal & orange cinematic grade",
        "aspect": "21:9",
    },
    "概念艺术": {
        "category": "场景",
        "tags": "concept art, matte painting, production design, pre-visualization",
        "quality": "ILM / weta concept sketch, narrative-driven composition",
        "neg": "finished illustration, cartoon, low detail",
        "camera": "cinematic wide establishing",
        "lighting": "narrative-lit hero with atmosphere",
        "palette": "mood-defined limited palette",
        "aspect": "21:9",
    },

    # ========== 游戏艺术 Game Art (v2.1 新增) ==========
    "原神": {
        "category": "游戏",
        "tags": "Genshin Impact style, miHoYo aesthetic, stylized anime rendering, cel shaded 3d",
        "quality": "gacha game hero card quality, detailed anime character portrait",
        "neg": "photorealistic, western cartoon, gritty",
        "camera": "3/4 character hero shot, slightly upward angle",
        "lighting": "rim light on hair, soft key + colored fill",
        "palette": "vibrant saturated anime palette, element-themed accents",
        "aspect": "3:4",
    },
    "崩铁星穹": {
        "category": "游戏",
        "tags": "Honkai Star Rail style, space fantasy JRPG anime, miHoYo rendering",
        "quality": "splash art quality, dynamic pose, elemental VFX",
        "neg": "photorealistic, rustic, medieval",
        "camera": "dynamic dutch angle hero shot",
        "lighting": "glowing elemental rim light",
        "palette": "cosmic gradient + neon accent",
        "aspect": "3:4",
    },
    "英雄联盟": {
        "category": "游戏",
        "tags": "League of Legends splash art style, Riot Games painterly illustration",
        "quality": "champion splash quality, dramatic action pose",
        "neg": "anime chibi, flat vector, photo",
        "camera": "dynamic low angle hero pose",
        "lighting": "dramatic rim with colored ability VFX",
        "palette": "saturated fantasy palette with magical accent",
        "aspect": "16:9",
    },
    "暗黑4": {
        "category": "游戏",
        "tags": "Diablo IV style, dark gothic fantasy, blizzard illustration",
        "quality": "ARPG splash quality, grim dark atmosphere",
        "neg": "cheerful, pastel, chibi, flat",
        "camera": "low-angle menacing hero shot",
        "lighting": "infernal red rim, volumetric fog",
        "palette": "charcoal black, ember red, corrupted green",
        "aspect": "3:2",
    },
    "Valorant": {
        "category": "游戏",
        "tags": "Valorant agent art, stylized flat anime realism, Riot FPS aesthetic",
        "quality": "agent reveal quality, confident hero pose",
        "neg": "painterly fantasy, chibi",
        "camera": "3/4 hero standoff",
        "lighting": "clean cel shaded with colored ability glow",
        "palette": "agent signature color + urban neutral",
        "aspect": "3:4",
    },
    "Pokemon": {
        "category": "游戏",
        "tags": "Pokemon style, Ken Sugimori illustration, round cute creature design",
        "quality": "Pokedex official art, clean cel shading",
        "neg": "gritty, realistic, complex anatomy",
        "camera": "3/4 creature portrait on white",
        "lighting": "flat cel shading with soft shadow",
        "palette": "clean primary colors per type",
        "aspect": "1:1",
    },
    "暴雪风": {
        "category": "游戏",
        "tags": "Blizzard stylized art, Overwatch / WoW concept style, exaggerated anatomy",
        "quality": "blizzard cinematic quality, heroic pose, strong silhouette",
        "neg": "photorealistic, anime chibi, flat",
        "camera": "heroic low angle, dynamic posing",
        "lighting": "dramatic three-point hero light",
        "palette": "rich saturated fantasy palette",
        "aspect": "3:2",
    },

    # ========== 东方传统 Chinese/Japanese Traditional (v2.1 新增) ==========
    "敦煌壁画": {
        "category": "东方",
        "tags": "Dunhuang mural style, Tang dynasty fresco, flying apsara figures, silk road art",
        "quality": "weathered ancient mural texture, mineral pigment on plaster",
        "neg": "modern digital, anime, western",
        "camera": "flat mural frontal view",
        "lighting": "no modeling light, flat pigment",
        "palette": "mineral ochre, malachite green, azurite blue, gold leaf",
        "aspect": "4:3",
    },
    "青花瓷": {
        "category": "东方",
        "tags": "Chinese blue and white porcelain motif, Ming dynasty pattern, cobalt underglaze",
        "quality": "porcelain surface detail, intricate floral motif",
        "neg": "full color, western, abstract",
        "camera": "",
        "lighting": "soft glazed porcelain highlight",
        "palette": "cobalt blue on pure white porcelain",
        "aspect": "1:1",
    },
    "民国月份牌": {
        "category": "东方",
        "tags": "Republic of China calendar poster, 1920s Shanghai art deco fusion, qipao glamour",
        "quality": "vintage advertising print, lithograph texture",
        "neg": "modern digital, anime, photo",
        "camera": "",
        "lighting": "flat poster illumination",
        "palette": "faded pastel with gold gilt accents",
        "aspect": "2:3",
    },
    "年画": {
        "category": "东方",
        "tags": "Chinese new year folk woodblock, auspicious symbols, chubby child figures",
        "quality": "traditional woodblock print texture, folk decorative",
        "neg": "photorealistic, minimalist, western",
        "camera": "",
        "lighting": "flat festive graphic",
        "palette": "festive vermillion, gold, pine green",
        "aspect": "3:4",
    },
    "剪纸": {
        "category": "东方",
        "tags": "Chinese paper cutting art, red paper silhouette, intricate symmetric cutout",
        "quality": "fine paper cut detail, traditional folk craft",
        "neg": "full color, 3d, photorealistic",
        "camera": "",
        "lighting": "flat silhouette with background paper",
        "palette": "pure vermillion red on neutral background",
        "aspect": "1:1",
    },
    "和风": {
        "category": "东方",
        "tags": "Japanese wafu aesthetic, traditional kimono elegance, wagashi sensibility",
        "quality": "refined Japanese traditional design",
        "neg": "western, modern pop, grunge",
        "camera": "",
        "lighting": "soft shoji-diffused light",
        "palette": "indigo, vermillion, sumi ink, cream washi",
        "aspect": "3:4",
    },
    "汉服写真": {
        "category": "东方",
        "tags": "hanfu photography, Chinese traditional dress, oriental portrait",
        "quality": "ethereal hanfu fashion editorial, flowing silk",
        "neg": "western dress, modern clothing, cyberpunk",
        "camera": "85mm portrait, soft 3/4",
        "lighting": "diffuse morning light, soft bounce",
        "palette": "silk ink tones, jade, cream, plum",
        "aspect": "3:4",
    },

    # ========== 动漫扩展 Anime extras (v2.1 新增) ==========
    "萌系": {
        "category": "动漫",
        "tags": "moe anime style, cute girl aesthetic, large sparkling eyes",
        "quality": "moekko illustration, clean lineart, rich anime shading",
        "neg": "gritty, adult, western comic",
        "camera": "",
        "lighting": "soft diffuse with catchlight in eyes",
        "palette": "pastel pink cream sky-blue",
        "aspect": "3:4",
    },
    "厚涂": {
        "category": "动漫",
        "tags": "painterly anime, thick paint anime illustration, semi-realistic rendering",
        "quality": "artstation anime painting, detailed brushwork",
        "neg": "flat cel shading, vector, chibi",
        "camera": "",
        "lighting": "rembrandt on face, painterly shadows",
        "palette": "desaturated muted painterly tones",
        "aspect": "3:4",
    },
    "轻小说封面": {
        "category": "动漫",
        "tags": "light novel cover illustration, Japanese LN art, glossy anime portrait",
        "quality": "bookshelf-ready cover composition, eye-catching character",
        "neg": "dark horror, western comic, 3d",
        "camera": "3/4 character hero, title-friendly negative space",
        "lighting": "cinematic anime key light",
        "palette": "vibrant anime palette with atmosphere",
        "aspect": "2:3",
    },
    "赛璐璐": {
        "category": "动漫",
        "tags": "traditional cel-shaded anime, sharp shadow boundaries, limited anime palette",
        "quality": "classic 2d cel animation look, detailed line art",
        "neg": "painterly, 3d render, gradient shading",
        "camera": "",
        "lighting": "two-tone cel shading, hard shadow edges",
        "palette": "saturated flat anime palette",
        "aspect": "16:9",
    },

    # ========== 现代设计 Modern Design (v2.1 新增) ==========
    "玻璃拟态": {
        "category": "设计",
        "tags": "glassmorphism, frosted glass UI, transparent blur layers, depth card stack",
        "quality": "modern UI glass effect, realistic refraction, clean layout",
        "neg": "flat 2d, skeuomorphic wood, pixel art",
        "camera": "",
        "lighting": "subtle inner glow, soft backlight through glass",
        "palette": "pastel gradient backdrop with translucent glass",
        "aspect": "3:4",
    },
    "新拟态": {
        "category": "设计",
        "tags": "neumorphism, soft UI, extruded plastic button, subtle dual shadow",
        "quality": "modern minimal UI, monochrome neumorphic elements",
        "neg": "flat, photorealistic, grunge",
        "camera": "",
        "lighting": "soft dual light and dark shadow",
        "palette": "monochrome beige or gray single-tone",
        "aspect": "1:1",
    },
    "孟菲斯": {
        "category": "设计",
        "tags": "Memphis design, 1980s postmodern, geometric shapes, squiggle pattern, bold primaries",
        "quality": "playful postmodern graphic, bold composition",
        "neg": "minimalist, photorealistic, classical",
        "camera": "",
        "lighting": "flat graphic, no modeling",
        "palette": "hot pink, cyan, yellow, black squiggle pattern",
        "aspect": "1:1",
    },
    "杂志编排": {
        "category": "设计",
        "tags": "editorial magazine layout, bold serif typography, grid-based design",
        "quality": "international typographic style, vogue spread quality",
        "neg": "amateur, overcluttered, cute",
        "camera": "",
        "lighting": "clean flat studio-style",
        "palette": "monochrome with single bold accent",
        "aspect": "3:4",
    },
    "包豪斯": {
        "category": "设计",
        "tags": "Bauhaus design, de stijl geometric, primary color blocks, constructivist",
        "quality": "1920s modernist design school, pure geometry",
        "neg": "ornate, victorian, realistic",
        "camera": "",
        "lighting": "flat geometric",
        "palette": "primary red yellow blue + black on white",
        "aspect": "1:1",
    },
    "奶油风": {
        "category": "设计",
        "tags": "cream style, soft beige palette, warm minimal aesthetic, korean lifestyle",
        "quality": "instagram lifestyle aesthetic, soft velvety texture",
        "neg": "dark, saturated, edgy",
        "camera": "",
        "lighting": "natural soft window light",
        "palette": "cream, soft beige, butter yellow, milk tea",
        "aspect": "4:5",
    },

    # ========== 建筑 & 氛围扩展 (v2.1 新增) ==========
    "粗野主义": {
        "category": "场景",
        "tags": "brutalist architecture, raw concrete, heavy geometric mass, béton brut",
        "quality": "mid-century brutalist landmark, imposing scale",
        "neg": "ornate, baroque, flimsy",
        "camera": "wide low-angle heroic architecture shot",
        "lighting": "harsh sun shadow across concrete",
        "palette": "raw concrete gray with sky contrast",
        "aspect": "16:9",
    },
    "北欧极简": {
        "category": "场景",
        "tags": "scandinavian interior, nordic minimalism, light wood, warm neutral",
        "quality": "hygge lifestyle, interior magazine quality",
        "neg": "ornate, cluttered, dark gothic",
        "camera": "wide 24mm interior architectural",
        "lighting": "large window natural light",
        "palette": "warm wood, white wall, soft gray",
        "aspect": "16:9",
    },
    "侘寂": {
        "category": "场景",
        "tags": "wabi-sabi aesthetic, imperfect natural beauty, weathered texture, zen japanese",
        "quality": "quiet imperfection, aged material detail",
        "neg": "glossy modern, bright colors, ornate",
        "camera": "",
        "lighting": "soft diffused natural, muted",
        "palette": "muted earth, weathered gray, aged beige",
        "aspect": "4:5",
    },

    # ========== 摄影扩展 (v2.1 新增) ==========
    "暗黑美食": {
        "category": "摄影",
        "tags": "dark food photography, moody cuisine, chiaroscuro plating",
        "quality": "michelin-level dark food styling, dramatic shadow",
        "neg": "bright cheerful, flat, cluttered",
        "camera": "100mm macro 45-degree, side low-key",
        "lighting": "single hard key from behind, deep shadow",
        "palette": "deep black with food color accent",
        "aspect": "4:5",
    },
    "日杂": {
        "category": "摄影",
        "tags": "Japanese lifestyle magazine, natural light still life, clean minimalism",
        "quality": "muji aesthetic, calm everyday beauty",
        "neg": "dark moody, dramatic, saturated",
        "camera": "50mm still life, slight top-down",
        "lighting": "soft window daylight, no drama",
        "palette": "cream, light wood, pale pastel",
        "aspect": "4:5",
    },
    "街头潮流": {
        "category": "摄影",
        "tags": "streetwear fashion, urban hypebeast, sneaker culture",
        "quality": "street style magazine editorial, confident pose",
        "neg": "formal suit, fantasy, kawaii",
        "camera": "35mm full body street fashion",
        "lighting": "harsh urban daylight or neon",
        "palette": "high contrast monochrome + brand accent",
        "aspect": "3:4",
    },

    # ========== 综合 (v2.1 新增) ==========
    "疗愈治愈": {
        "category": "场景",
        "tags": "healing cozy aesthetic, soft warm interior, cat sunlight, tea steam",
        "quality": "soothing slow-life scene",
        "neg": "dramatic action, dark, cyberpunk",
        "camera": "",
        "lighting": "warm golden hour through window",
        "palette": "warm honey, cream, dusty pink",
        "aspect": "4:5",
    },
    "美式复古": {
        "category": "场景",
        "tags": "americana retro, 1950s diner, vintage coca-cola americana",
        "quality": "Norman Rockwell meets mid-century ad",
        "neg": "asian, modern sleek, futuristic",
        "camera": "",
        "lighting": "warm diner fluorescent or golden",
        "palette": "cherry red, cream, turquoise",
        "aspect": "3:2",
    },
}

# ─────────────────────────────────────────────────────────
# 别名 (英文 / 同义词 → 规范预设名)
# ─────────────────────────────────────────────────────────
ALIASES: Dict[str, str] = {
    # 英文
    "realistic": "写实摄影",
    "photo": "写实摄影",
    "photography": "写实摄影",
    "film": "胶片摄影",
    "analog": "胶片摄影",
    "bw": "黑白摄影",
    "blackwhite": "黑白摄影",
    "monochrome": "黑白摄影",
    "portrait": "人像摄影",
    "fashion": "时尚大片",
    "editorial": "时尚大片",
    "food": "美食摄影",
    "product": "产品摄影",
    "ecommerce": "产品摄影",
    "macro": "微距摄影",
    "aerial": "航拍摄影",
    "drone": "航拍摄影",
    "street": "街拍纪实",
    "documentary": "街拍纪实",
    "anime": "动漫",
    "ghibli": "宫崎骏",
    "miyazaki": "宫崎骏",
    "shinkai": "新海诚",
    "makoto": "新海诚",
    "comic": "美漫",
    "marvel": "美漫",
    "chibi": "Q版",
    "kawaii": "Q版",
    "storybook": "童话绘本",
    "childrensbook": "童话绘本",
    "watercolor": "水彩",
    "oil": "油画",
    "ink": "水墨",
    "sumi": "水墨",
    "gongbi": "工笔国画",
    "ukiyoe": "浮世绘",
    "lineart": "线稿",
    "pixel": "像素艺术",
    "3d": "3DC4D",
    "c4d": "3DC4D",
    "octane": "3DC4D",
    "blindbox": "盲盒手办",
    "popmart": "盲盒手办",
    "lowpoly": "低多边形",
    "isometric": "等距视图",
    "iso": "等距视图",
    "claymation": "粘土",
    "felt": "毛毡手工",
    "papercraft": "纸艺",
    "minimal": "极简主义",
    "minimalist": "极简主义",
    "flat": "平面设计",
    "vector": "平面设计",
    "logo": "Logo设计",
    "icon": "图标设计",
    "infographic": "信息图",
    "kv": "品牌KV",
    "album": "专辑封面",
    "poster": "复古海报",
    "movieposter": "电影海报",
    "sticker": "表情包",
    "emoji": "表情包",
    "impressionist": "印象派",
    "vangogh": "后印象派",
    "postimpressionist": "后印象派",
    "artnouveau": "新艺术",
    "mucha": "新艺术",
    "artdeco": "装饰艺术",
    "cyberpunk": "赛博朋克",
    "steampunk": "蒸汽朋克",
    "scifi": "科幻",
    "fantasy": "奇幻",
    "darkfantasy": "黑暗奇幻",
    "grimdark": "黑暗奇幻",
    "guochao": "国潮",
    "y2k": "Y2K",
    "vaporwave": "Vaporwave",
    "synthwave": "Vaporwave",
    "neon": "霓虹灯牌",
    "archviz": "建筑可视化",
    "architecture": "建筑可视化",
    "cinematic": "电影感",
    "cinema": "电影感",
    "concept": "概念艺术",
    "conceptart": "概念艺术",
    # v2.1 游戏
    "genshin": "原神",
    "mihoyo": "原神",
    "honkai": "崩铁星穹",
    "starrail": "崩铁星穹",
    "lol": "英雄联盟",
    "leagueoflegends": "英雄联盟",
    "diablo": "暗黑4",
    "valorant": "Valorant",
    "pokemon": "Pokemon",
    "blizzard": "暴雪风",
    "overwatch": "暴雪风",
    "wow": "暴雪风",
    # v2.1 东方
    "dunhuang": "敦煌壁画",
    "qinghua": "青花瓷",
    "porcelain": "青花瓷",
    "yuefenpai": "民国月份牌",
    "wafu": "和风",
    "hanfu": "汉服写真",
    "papercut": "剪纸",
    "nianhua": "年画",
    # v2.1 动漫扩展
    "moe": "萌系",
    "painterlyanime": "厚涂",
    "lightnovel": "轻小说封面",
    "lncover": "轻小说封面",
    "cellshaded": "赛璐璐",
    "celshaded": "赛璐璐",
    # v2.1 设计
    "glassmorphism": "玻璃拟态",
    "glass": "玻璃拟态",
    "neumorphism": "新拟态",
    "memphis": "孟菲斯",
    "editorial": "杂志编排",
    "bauhaus": "包豪斯",
    "cream": "奶油风",
    "korean": "奶油风",
    # v2.1 建筑 / 氛围
    "brutalism": "粗野主义",
    "brutalist": "粗野主义",
    "nordic": "北欧极简",
    "scandinavian": "北欧极简",
    "wabisabi": "侘寂",
    "zen": "侘寂",
    # v2.1 摄影
    "darkfood": "暗黑美食",
    "muji": "日杂",
    "streetwear": "街头潮流",
    "hypebeast": "街头潮流",
    # v2.1 综合
    "healing": "疗愈治愈",
    "cozy": "疗愈治愈",
    "americana": "美式复古",
}


# ─────────────────────────────────────────────────────────
# 意图关键词 → (推荐预设, 推荐画幅)
# ─────────────────────────────────────────────────────────
INTENT_KEYWORDS: List[Tuple[str, str, str]] = [
    # (关键词, 推荐预设, 推荐画幅)
    ("logo", "Logo设计", "1:1"),
    ("徽标", "Logo设计", "1:1"),
    ("标志", "Logo设计", "1:1"),
    ("icon", "图标设计", "1:1"),
    ("图标", "图标设计", "1:1"),
    ("app图标", "图标设计", "1:1"),
    ("电影海报", "电影海报", "2:3"),
    ("海报", "复古海报", "3:4"),
    ("poster", "复古海报", "3:4"),
    ("封面", "专辑封面", "1:1"),
    ("专辑", "专辑封面", "1:1"),
    ("表情包", "表情包", "1:1"),
    ("贴纸", "表情包", "1:1"),
    ("信息图", "信息图", "3:4"),
    ("infographic", "信息图", "3:4"),
    ("kv", "品牌KV", "16:9"),
    ("主视觉", "品牌KV", "16:9"),
    ("产品", "产品摄影", "1:1"),
    ("电商", "产品摄影", "1:1"),
    ("商品", "产品摄影", "1:1"),
    ("美食", "美食摄影", "1:1"),
    ("食物", "美食摄影", "1:1"),
    ("菜品", "美食摄影", "1:1"),
    ("头像", "人像摄影", "1:1"),
    ("肖像", "人像摄影", "3:4"),
    ("人像", "人像摄影", "3:4"),
    ("时装", "时尚大片", "3:4"),
    ("时尚", "时尚大片", "3:4"),
    ("街拍", "街拍纪实", "3:2"),
    ("纪实", "街拍纪实", "3:2"),
    ("风景", "写实摄影", "16:9"),
    ("风光", "写实摄影", "16:9"),
    ("建筑", "建筑可视化", "16:9"),
    ("室内", "建筑可视化", "4:3"),
    ("手办", "盲盒手办", "1:1"),
    ("盲盒", "盲盒手办", "1:1"),
    ("玩具", "盲盒手办", "1:1"),
    ("航拍", "航拍摄影", "16:9"),
    ("鸟瞰", "航拍摄影", "16:9"),
    ("微距", "微距摄影", "1:1"),
    ("赛博", "赛博朋克", "21:9"),
    ("cyberpunk", "赛博朋克", "21:9"),
    ("蒸汽朋克", "蒸汽朋克", "3:2"),
    ("科幻", "科幻", "21:9"),
    ("未来", "科幻", "21:9"),
    ("奇幻", "奇幻", "16:9"),
    ("魔幻", "奇幻", "16:9"),
    ("黑暗", "黑暗奇幻", "2:3"),
    ("水墨", "水墨", "3:4"),
    ("国画", "工笔国画", "3:4"),
    ("工笔", "工笔国画", "3:4"),
    ("浮世绘", "浮世绘", "2:3"),
    ("童话", "童话绘本", "4:3"),
    ("绘本", "童话绘本", "4:3"),
    ("宫崎骏", "宫崎骏", "16:9"),
    ("新海诚", "新海诚", "16:9"),
    ("动漫", "动漫", "3:4"),
    ("二次元", "动漫", "3:4"),
    ("q版", "Q版", "1:1"),
    ("Q版", "Q版", "1:1"),
    ("chibi", "Q版", "1:1"),
    ("线稿", "线稿", "1:1"),
    ("像素", "像素艺术", "1:1"),
    ("3d", "3DC4D", "1:1"),
    ("c4d", "3DC4D", "1:1"),
    ("粘土", "粘土", "1:1"),
    ("等距", "等距视图", "1:1"),
    ("国潮", "国潮", "3:4"),
    ("霓虹", "霓虹灯牌", "3:2"),
    ("电影", "电影感", "21:9"),
    ("cinema", "电影感", "21:9"),
    ("concept", "概念艺术", "21:9"),
    ("概念图", "概念艺术", "21:9"),
    ("复古", "复古海报", "3:4"),
    ("vintage", "复古海报", "3:4"),
    # v2.1 游戏
    ("原神", "原神", "3:4"),
    ("genshin", "原神", "3:4"),
    ("崩铁", "崩铁星穹", "3:4"),
    ("星穹", "崩铁星穹", "3:4"),
    ("lol", "英雄联盟", "16:9"),
    ("英雄联盟", "英雄联盟", "16:9"),
    ("valorant", "Valorant", "3:4"),
    ("暗黑4", "暗黑4", "3:2"),
    ("diablo", "暗黑4", "3:2"),
    ("pokemon", "Pokemon", "1:1"),
    ("宝可梦", "Pokemon", "1:1"),
    ("暴雪", "暴雪风", "3:2"),
    ("overwatch", "暴雪风", "3:2"),
    # v2.1 东方
    ("敦煌", "敦煌壁画", "4:3"),
    ("壁画", "敦煌壁画", "4:3"),
    ("青花瓷", "青花瓷", "1:1"),
    ("月份牌", "民国月份牌", "2:3"),
    ("民国", "民国月份牌", "2:3"),
    ("剪纸", "剪纸", "1:1"),
    ("年画", "年画", "3:4"),
    ("汉服", "汉服写真", "3:4"),
    ("和风", "和风", "3:4"),
    ("日系", "日杂", "4:5"),
    ("日杂", "日杂", "4:5"),
    # v2.1 动漫扩展
    ("萌", "萌系", "3:4"),
    ("萌系", "萌系", "3:4"),
    ("厚涂", "厚涂", "3:4"),
    ("轻小说", "轻小说封面", "2:3"),
    ("赛璐璐", "赛璐璐", "16:9"),
    # v2.1 现代设计
    ("玻璃拟态", "玻璃拟态", "3:4"),
    ("glassmorphism", "玻璃拟态", "3:4"),
    ("新拟态", "新拟态", "1:1"),
    ("neumorphism", "新拟态", "1:1"),
    ("孟菲斯", "孟菲斯", "1:1"),
    ("memphis", "孟菲斯", "1:1"),
    ("杂志", "杂志编排", "3:4"),
    ("magazine", "杂志编排", "3:4"),
    ("包豪斯", "包豪斯", "1:1"),
    ("bauhaus", "包豪斯", "1:1"),
    ("奶油", "奶油风", "4:5"),
    ("ins风", "奶油风", "4:5"),
    ("韩系", "奶油风", "4:5"),
    # v2.1 建筑 / 氛围
    ("粗野", "粗野主义", "16:9"),
    ("brutalism", "粗野主义", "16:9"),
    ("北欧", "北欧极简", "16:9"),
    ("scandinavian", "北欧极简", "16:9"),
    ("侘寂", "侘寂", "4:5"),
    ("wabi", "侘寂", "4:5"),
    ("禅意", "侘寂", "4:5"),
    # v2.1 摄影
    ("暗黑美食", "暗黑美食", "4:5"),
    ("darkfood", "暗黑美食", "4:5"),
    ("街头", "街头潮流", "3:4"),
    ("潮牌", "街头潮流", "3:4"),
    ("streetwear", "街头潮流", "3:4"),
    # v2.1 综合
    ("治愈", "疗愈治愈", "4:5"),
    ("疗愈", "疗愈治愈", "4:5"),
    ("cozy", "疗愈治愈", "4:5"),
    ("美式复古", "美式复古", "3:2"),
    ("americana", "美式复古", "3:2"),
]

# ─────────────────────────────────────────────────────────
# 构图关键词
# ─────────────────────────────────────────────────────────
COMPOSITION_KEYWORDS: Dict[str, str] = {
    "特写": "extreme close-up shot",
    "近景": "close-up shot",
    "中景": "medium shot",
    "全身": "full body shot",
    "半身": "medium shot, waist up",
    "远景": "wide shot, establishing shot",
    "全景": "panoramic view",
    "俯拍": "top-down view",
    "俯视": "top-down view",
    "仰拍": "low angle shot, looking up",
    "仰视": "low angle shot, looking up",
    "鸟瞰": "bird's eye view, aerial",
    "平视": "eye-level shot",
    "正面": "front view",
    "侧面": "side profile view",
    "背面": "back view",
    "三分之二": "three-quarter view",
}

# ─────────────────────────────────────────────────────────
# 情绪关键词
# ─────────────────────────────────────────────────────────
MOOD_KEYWORDS: Dict[str, str] = {
    "温暖": "warm cozy atmosphere, golden tones",
    "温馨": "warm cozy atmosphere, golden tones",
    "冷峻": "cold atmosphere, steely blue tones",
    "神秘": "mysterious mood, foggy, dim lighting",
    "梦幻": "dreamy ethereal mood, soft glow, bokeh",
    "欢快": "joyful vibrant, bright cheerful colors",
    "忧郁": "melancholic mood, muted cool palette",
    "压抑": "oppressive mood, deep shadows, heavy atmosphere",
    "史诗": "epic grandeur, cinematic scale",
    "高级": "luxury sophistication, premium materials",
    "治愈": "healing soft ambiance, soothing",
    "清新": "fresh airy light pastel",
    "紧张": "tense suspense mood, high contrast",
    "浪漫": "romantic soft pink glow",
}

# ─────────────────────────────────────────────────────────
# 时间 / 天气 / 季节 关键词（v2.1 新增）
# ─────────────────────────────────────────────────────────
TIME_KEYWORDS: Dict[str, str] = {
    "清晨": "early morning, dawn, soft first light",
    "早晨": "morning light, fresh daylight",
    "上午": "bright morning sunshine",
    "正午": "high noon, overhead sun",
    "下午": "afternoon light, long soft shadows",
    "黄昏": "dusk, golden hour, magic hour",
    "傍晚": "dusk, golden hour, magic hour",
    "日落": "sunset, golden hour",
    "夜晚": "night time, dark ambient",
    "深夜": "late night, moonlit, dim",
    "午夜": "midnight, dark sky",
    "黎明": "dawn, blue hour breaking",
    "蓝调时刻": "blue hour, twilight gradient sky",
    "魔法时刻": "magic hour, warm golden glow",
}

WEATHER_KEYWORDS: Dict[str, str] = {
    "晴天": "sunny clear sky",
    "多云": "cloudy overcast sky",
    "阴天": "overcast gray sky",
    "下雨": "raining, wet reflective surfaces",
    "雨天": "rainy weather, soft rain",
    "大雨": "heavy rain, downpour, water droplets",
    "暴雨": "stormy rain, dramatic weather",
    "下雪": "snowing, snowflakes in air",
    "雪天": "snowy landscape, white blanket",
    "暴雪": "blizzard, heavy snow storm",
    "雾天": "foggy misty atmosphere",
    "有雾": "foggy misty atmosphere",
    "晨雾": "morning mist, dreamy fog",
    "风暴": "stormy weather, dramatic clouds",
    "雷雨": "thunderstorm, lightning in sky",
}

SEASON_KEYWORDS: Dict[str, str] = {
    "春天": "spring season, cherry blossoms, fresh green",
    "春季": "spring season, cherry blossoms, fresh green",
    "夏天": "summer season, lush greenery, warm sun",
    "夏季": "summer season, lush greenery, warm sun",
    "秋天": "autumn season, golden foliage, maple leaves",
    "秋季": "autumn season, golden foliage, maple leaves",
    "冬天": "winter season, snow, bare branches",
    "冬季": "winter season, snow, bare branches",
    "樱花季": "cherry blossom season, sakura petals falling",
    "枫叶季": "maple season, red foliage",
}

# ─────────────────────────────────────────────────────────
# 质量档位（v2.1 新增）
# ─────────────────────────────────────────────────────────
QUALITY_TIERS: Dict[str, str] = {
    "basic": "high quality, detailed",
    "pro": "masterpiece, best quality, ultra detailed, 8k",
    "master": "masterpiece, best quality, ultra detailed, 8k, hdr, "
              "intricate details, sharp focus, award winning, trending on artstation, "
              "professional, highly polished",
}

# ─────────────────────────────────────────────────────────
# 负向需求识别（v2.1 新增）
# 匹配 "不要X" / "no X" / "avoid X" / "without X" / "没有X" / "避免X"
# ─────────────────────────────────────────────────────────
NEGATIVE_PATTERNS = [
    re.compile(r"不要([^,，。.;；]{1,20})"),
    re.compile(r"没有([^,，。.;；]{1,20})"),
    re.compile(r"避免([^,，。.;；]{1,20})"),
    re.compile(r"\bno\s+([a-zA-Z\s]{1,30})(?=[,.]|\s*$)"),
    re.compile(r"\bavoid\s+([a-zA-Z\s]{1,30})(?=[,.]|\s*$)"),
    re.compile(r"\bwithout\s+([a-zA-Z\s]{1,30})(?=[,.]|\s*$)"),
]

# ─────────────────────────────────────────────────────────
# 画幅 → 模型特定写法
# ─────────────────────────────────────────────────────────
ASPECT_TO_MJ = {
    "1:1": "--ar 1:1",
    "3:4": "--ar 3:4",
    "4:3": "--ar 4:3",
    "3:2": "--ar 3:2",
    "2:3": "--ar 2:3",
    "16:9": "--ar 16:9",
    "9:16": "--ar 9:16",
    "21:9": "--ar 21:9",
    "4:5": "--ar 4:5",
}

ASPECT_TO_SDXL = {
    "1:1": "1024x1024",
    "3:4": "896x1152",
    "4:3": "1152x896",
    "3:2": "1216x832",
    "2:3": "832x1216",
    "16:9": "1344x768",
    "9:16": "768x1344",
    "21:9": "1536x640",
    "4:5": "912x1144",
}


# ─────────────────────────────────────────────────────────
# 工具函数
# ─────────────────────────────────────────────────────────
def resolve_preset(name: Optional[str]) -> str:
    """预设名归一化：支持中文 / 英文别名 / 大小写不敏感。"""
    if not name:
        return ""
    key = name.strip().lower().replace(" ", "").replace("-", "").replace("_", "")
    if key in ALIASES:
        return ALIASES[key]
    for p in STYLE_PRESETS:
        if p.lower() == key or p.lower().replace(" ", "") == key:
            return p
    return name if name in STYLE_PRESETS else ""


def parse_requirement(text: str) -> Dict[str, str]:
    """从用户输入中解析意图、画幅、构图、情绪、时间、天气、季节、负向需求。

    返回 dict 字段：
        preset_suggestion  推荐预设（可能为空）
        aspect_suggestion  推荐画幅
        composition        构图片段（英文，可为空）
        mood               情绪片段（英文，可为空）
        time_of_day        时间片段（英文，可为空）
        weather            天气片段（英文，可为空）
        season             季节片段（英文，可为空）
        user_negatives     用户抽出的负向关键词（原文，英/中）
    """
    lower = text.lower()
    out = {
        "preset_suggestion": "",
        "aspect_suggestion": "",
        "composition": "",
        "mood": "",
        "time_of_day": "",
        "weather": "",
        "season": "",
        "user_negatives": [],
    }

    for kw, preset, aspect in INTENT_KEYWORDS:
        if kw.lower() in lower:
            out["preset_suggestion"] = preset
            out["aspect_suggestion"] = aspect
            break

    for zh, en in COMPOSITION_KEYWORDS.items():
        if zh in text:
            out["composition"] = en
            break

    for zh, en in MOOD_KEYWORDS.items():
        if zh in text:
            out["mood"] = en
            break

    for zh, en in TIME_KEYWORDS.items():
        if zh in text:
            out["time_of_day"] = en
            break

    for zh, en in WEATHER_KEYWORDS.items():
        if zh in text:
            out["weather"] = en
            break

    for zh, en in SEASON_KEYWORDS.items():
        if zh in text:
            out["season"] = en
            break

    # 负向需求抽取
    negs: List[str] = []
    for pat in NEGATIVE_PATTERNS:
        for m in pat.finditer(text):
            [REDACTED_SECRET]).strip().rstrip(",.， ；;")
            if token and token not in negs:
                negs.append(token)
    out["user_negatives"] = negs

    return out


def strip_negative_clauses(text: str) -> str:
    """从主体描述中去除 "不要X" 类子句，只保留正向描述。"""
    cleaned = text
    for pat in NEGATIVE_PATTERNS:
        cleaned = pat.sub("", cleaned)
    # 清理多余标点和空白
    cleaned = re.sub(r"\s*,\s*,+", ", ", cleaned)
    cleaned = re.sub(r"\s+", " ", cleaned).strip(" ,，。.;；")
    return cleaned


def sanitize_subject(text: str) -> str:
    """清理主体描述：去除首尾标点和多余空白。"""
    return re.sub(r"\s+", " ", text).strip().rstrip(".,，、。;；")


def stable_seed(subject: str, preset: str) -> int:
    """根据主体 + 预设生成稳定的种子建议（32-bit 正整数）。"""
    h = hashlib.md5(f"{subject}|{preset}".encode("utf-8")).hexdigest()
    return int(h[:8], 16)


def parse_mix_preset(preset_arg: str) -> Tuple[str, Optional[str]]:
    """支持 `-p A+B` 语法。返回 (primary, secondary or None)。"""
    if not preset_arg:
        return "", None
    if "+" not in preset_arg:
        return preset_arg, None
    parts = [p.strip() for p in preset_arg.split("+", 1)]
    if len(parts) != 2 or not parts[0] or not parts[1]:
        return preset_arg, None
    return parts[0], parts[1]


def mix_presets(primary: str, secondary: str, ratio: float = 0.6, model: str = "通用") -> Dict[str, str]:
    """加权融合两个预设，主预设 ratio，副预设 1-ratio。

    融合策略：
        tags     按权重前置主预设标签，SD 模式额外加 (tag:weight) 语法
        quality  主预设主导
        neg      合并去重
        camera   主预设（主导镜头语言）
        lighting 主预设主导，副预设为辅
        palette  混合两者（主在前）
        aspect   主预设
        category mix
    """
    p1 = STYLE_PRESETS[primary]
    p2 = STYLE_PRESETS[secondary]
    ratio = max(0.1, min(0.9, ratio))

    primary_tags = [t.strip() for t in p1["tags"].split(",") if t.strip()]
    secondary_tags = [t.strip() for t in p2["tags"].split(",") if t.strip()]

    is_sd = model in ("Stable Diffusion", "SD", "sd", "SDXL", "sdxl")
    if is_sd:
        w1 = round(0.8 + ratio * 0.6, 2)
        w2 = round(0.8 + (1 - ratio) * 0.6, 2)
        merged_tags = [f"({t}:{w1})" for t in primary_tags] + [f"({t}:{w2})" for t in secondary_tags]
    else:
        n1 = max(1, int(round(len(primary_tags) * (0.5 + ratio))))
        n2 = max(1, int(round(len(secondary_tags) * (0.5 + (1 - ratio)))))
        merged_tags = primary_tags[:n1] + secondary_tags[:n2]

    merged_palette = ", ".join([
        x for x in [p1.get("palette", ""), p2.get("palette", "")] if x
    ])
    if p1.get("lighting") and p2.get("lighting"):
        merged_lighting = f"{p1['lighting']}, blended with {p2['lighting']}"
    else:
        merged_lighting = p1.get("lighting") or p2.get("lighting", "")

    neg_tokens = []
    seen = set()
    for src in (p1["neg"], p2["neg"]):
        for t in src.split(","):
            t = t.strip()
            if t and t.lower() not in seen:
                seen.add(t.lower())
                neg_tokens.append(t)

    return {
        "category": f"{p1['category']}+{p2['category']}",
        "tags": ", ".join(merged_tags),
        "quality": p1["quality"],
        "neg": ", ".join(neg_tokens),
        "camera": p1.get("camera", "") or p2.get("camera", ""),
        "lighting": merged_lighting,
        "palette": merged_palette,
        "aspect": p1.get("aspect", "1:1"),
    }


def build_prompt(
    subject: str,
    preset: str,
    model: str = "通用",
    aspect: str = "",
    extra_mood: str = "",
    extra_composition: str = "",
    extra_negatives: str = "",
    seed: Optional[int] = None,
    quality_tier: str = "pro",
    character_sheet: bool = False,
    mix_secondary: Optional[str] = None,
    mix_ratio: float = 0.6,
) -> Dict:
    """构建增强后的提示词。

    v2.1 新增参数：
        extra_negatives  额外负面词，逗号分隔
        quality_tier     质量档位 basic / pro / master
        character_sheet  角色设定图模式（T-pose 多视图）
    v2.2 新增参数：
        mix_secondary    副预设名（已 resolve），与主预设融合
        mix_ratio        主预设权重 0.1-0.9
    """
    preset = resolve_preset(preset) or "写实摄影"
    if mix_secondary:
        mix_secondary = resolve_preset(mix_secondary) or ""
    if mix_secondary and mix_secondary != preset:
        data = mix_presets(preset, mix_secondary, mix_ratio, model)
        mixed_label = f"{preset}+{mix_secondary}@{mix_ratio:.2f}"
    else:
        data = STYLE_PRESETS[preset]
        mixed_label = ""

    auto = parse_requirement(subject)
    subject_clean = sanitize_subject(strip_negative_clauses(subject))

    if not extra_composition:
        extra_composition = auto["composition"]
    if not extra_mood:
        extra_mood = auto["mood"]
    if not aspect:
        aspect = data.get("aspect", "1:1")

    # 时间 / 天气 / 季节
    ambient_parts = [auto["time_of_day"], auto["weather"], auto["season"]]
    ambient = ", ".join([x for x in ambient_parts if x])

    # 角色设定图模式
    if character_sheet:
        subject_clean = (
            f"character design sheet of {subject_clean}, "
            f"multiple views: front view, three-quarter view, side view, back view, "
            f"T-pose, clean white background, reference sheet, "
            f"consistent character design"
        )
        aspect = "16:9"

    consistency_parts = [
        data["tags"],
        data.get("camera", ""),
        data.get("lighting", ""),
        data.get("palette", ""),
    ]
    consistency = ", ".join([x for x in consistency_parts if x])

    # 质量档位（替换 UNIVERSAL_QUALITY）
    tier_quality = QUALITY_TIERS.get(quality_tier, QUALITY_TIERS["pro"])
    quality_combined = f"{data['quality']}, {tier_quality}"

    # 负面词：预设 + 全局过滤 + 用户抽出 + 显式追加
    neg_exclude = list(PRESET_NEG_EXCLUDE.get(preset, []))
    if mix_secondary and mix_secondary in PRESET_NEG_EXCLUDE:
        neg_exclude.extend(PRESET_NEG_EXCLUDE[mix_secondary])
    universal_neg_filtered = _filter_neg(UNIVERSAL_NEG, neg_exclude)
    user_neg_from_subject = ", ".join(auto["user_negatives"])
    neg_parts = [data["neg"], universal_neg_filtered, user_neg_from_subject, extra_negatives]
    neg_combined = ", ".join([x for x in neg_parts if x])

    extras = ", ".join([x for x in [extra_composition, extra_mood, ambient] if x])

    seed_key = mixed_label or preset

    # 按模型生成不同形式
    if model in ("Midjourney", "MJ", "mj"):
        core = f"{subject_clean}, {consistency}"
        if extras:
            core = f"{core}, {extras}"
        core = f"{core}, {quality_combined}"
        flags = [ASPECT_TO_MJ.get(aspect, "--ar 1:1"), "--stylize 250"]
        positive = f"{core} {' '.join(flags)}"
        negative = f"--no {neg_combined}"
        hint = (
            "Midjourney tips：\n"
            "  • 角色/产品系列一致：加 --cref <url> 或 --sref <url>\n"
            f"  • 想要更风格化加 --stylize 500~750；更写实降到 --stylize 50\n"
            f"  • 建议 seed 锁定：--seed {seed or stable_seed(subject_clean, seed_key)}"
        )

    elif model in ("Stable Diffusion", "SD", "sd"):
        positive = (
            f"({subject_clean}:1.2), {consistency}"
            + (f", {extras}" if extras else "")
            + f", {quality_combined}"
        )
        negative = neg_combined
        hint = (
            "Stable Diffusion tips：\n"
            f"  • 强化权重: (word:1.2~1.5), 减弱: [word:0.7]\n"
            f"  • 建议尺寸 (SD 1.5): 512x{{hw_from_aspect}}; (SDXL): {ASPECT_TO_SDXL.get(aspect,'1024x1024')}\n"
            f"  • 采样: DPM++ 2M Karras, 30 steps, CFG 6.5\n"
            f"  • 建议 seed 锁定: {seed or stable_seed(subject_clean, seed_key)}（系列同 seed 提升一致性）"
        )

    elif model in ("SDXL", "sdxl"):
        positive = (
            f"{subject_clean}, {consistency}"
            + (f", {extras}" if extras else "")
            + f", {quality_combined}"
        )
        negative = neg_combined
        hint = (
            "SDXL tips：\n"
            f"  • 推荐尺寸: {ASPECT_TO_SDXL.get(aspect,'1024x1024')}\n"
            f"  • 采样: DPM++ SDE Karras, 25-30 steps, CFG 5-7\n"
            f"  • Refiner 使用率 0.2-0.3\n"
            f"  • seed: {seed or stable_seed(subject_clean, seed_key)}"
        )

    elif model in ("DALL-E", "DALL·E", "dalle", "DALLE"):
        parts = [f"A {preset} style image of {subject_clean}"]
        if data.get("camera"):
            parts.append(f"captured with {data['camera']}")
        if data.get("lighting"):
            parts.append(f"lit by {data['lighting']}")
        if data.get("palette"):
            parts.append(f"with a color palette of {data['palette']}")
        if extras:
            parts.append(extras)
        parts.append("highly detailed, professional composition")
        positive = ". ".join(parts) + "."
        negative = "(DALL-E 3 忽略负面提示，已通过正向描述规避)"
        hint = (
            "DALL-E 3 tips：\n"
            "  • 用自然语言句子 + 细节形容词效果最佳\n"
            f"  • 画幅: {aspect} (仅支持 1:1, 16:9, 9:16 在 ChatGPT 内)\n"
            "  • 一致性: 在同一会话连续生成并引用 \"use the same character\""
        )

    elif model in ("Flux", "flux"):
        positive = (
            f"{subject_clean}. {consistency}."
            + (f" {extras}." if extras else "")
            + f" {quality_combined}."
        )
        negative = neg_combined
        hint = (
            "Flux tips：\n"
            "  • 支持长自然语言提示，可加句式结构 \"The subject is...\"\n"
            f"  • 建议 Flux Dev: guidance 3.5; Flux Schnell: guidance 0\n"
            f"  • seed: {seed or stable_seed(subject_clean, seed_key)}"
        )

    else:  # 通用
        positive = (
            f"{subject_clean}, {consistency}"
            + (f", {extras}" if extras else "")
            + f", {quality_combined}"
        )
        negative = neg_combined
        hint = "通用格式：Midjourney / SD / Flux 皆可直接使用。"

    return {
        "version": VERSION,
        "original": subject,
        "preset": preset,
        "mix_secondary": mix_secondary or "",
        "mix_ratio": mix_ratio if mix_secondary else None,
        "mix_label": mixed_label,
        "model": model,
        "aspect": aspect,
        "composition": extra_composition,
        "mood": extra_mood,
        "time_of_day": auto.get("time_of_day", ""),
        "weather": auto.get("weather", ""),
        "season": auto.get("season", ""),
        "quality_tier": quality_tier,
        "character_sheet": character_sheet,
        "user_negatives": auto.get("user_negatives", []),
        "seed_suggestion": seed or stable_seed(subject_clean, seed_key),
        "positive": positive,
        "negative": negative,
        "hint": hint,
        "consistency_lock": {
            "camera": data.get("camera", ""),
            "lighting": data.get("lighting", ""),
            "palette": data.get("palette", ""),
            "aspect": aspect,
        },
    }


def build_series(
    subject: str,
    preset: str,
    model: str,
    aspect: str,
    variations: List[str],
    seed: Optional[int] = None,
    quality_tier: str = "pro",
    mix_secondary: Optional[str] = None,
    mix_ratio: float = 0.6,
) -> List[Dict]:
    """系列批量生成：共享 camera/lighting/palette/seed 锁，仅替换主体描述。"""
    if seed is None:
        seed_key = f"{preset}+{mix_secondary}@{mix_ratio:.2f}" if mix_secondary else preset
        seed = stable_seed(subject, seed_key)
    results = []
    for i, v in enumerate(variations, 1):
        full = f"{subject}, {v}" if v and v != subject else subject
        r = build_prompt(
            full, preset, model, aspect, seed=seed, quality_tier=quality_tier,
            mix_secondary=mix_secondary, mix_ratio=mix_ratio,
        )
        r["series_index"] = i
        r["series_total"] = len(variations)
        results.append(r)
    return results


# ─────────────────────────────────────────────────────────
# 输出
# ─────────────────────────────────────────────────────────
def print_prompt(result: Dict):
    sep = "═" * 60
    print(f"\n{sep}")
    if "series_index" in result:
        print(f"📸 系列生成 [{result['series_index']}/{result['series_total']}]")
    if result.get("character_sheet"):
        print("👤 角色设定图模式：T-pose 多视图（喂给 MJ --cref / IP-Adapter）")
    print(f"📌 原始描述   : {result['original']}")
    if result.get("mix_label"):
        print(f"🎨 风格预设   : {result['mix_label']} (混合)")
    else:
        print(f"🎨 风格预设   : {result['preset']}")
    print(f"🤖 目标模型   : {result['model']}")
    print(f"📐 画幅       : {result['aspect']}")
    print(f"⭐ 质量档位   : {result.get('quality_tier', 'pro')}")
    if result.get("composition"):
        print(f"🎥 构图       : {result['composition']}")
    if result.get("mood"):
        print(f"🎭 情绪       : {result['mood']}")
    if result.get("time_of_day"):
        print(f"🕐 时间       : {result['time_of_day']}")
    if result.get("weather"):
        print(f"☁️  天气       : {result['weather']}")
    if result.get("season"):
        print(f"🍂 季节       : {result['season']}")
    if result.get("user_negatives"):
        print(f"🚫 用户负向   : {', '.join(result['user_negatives'])}  → 已入负面")
    print(f"🎲 种子建议   : {result['seed_suggestion']}")
    print(f"\n✅ 正向提示词：")
    print(f"{result['positive']}")
    print(f"\n❌ 负向提示词：")
    print(f"{result['negative']}")
    print(f"\n🔒 一致性锁：")
    for k, v in result["consistency_lock"].items():
        if v:
            print(f"   {k:8s}: {v}")
    print(f"\n💡 {result['hint']}")
    print(f"{sep}\n")


def list_presets():
    by_cat: Dict[str, List[str]] = {}
    for name, data in STYLE_PRESETS.items():
        by_cat.setdefault(data["category"], []).append(name)
    print(f"\n🎨 可用风格预设 (共 {len(STYLE_PRESETS)} 款)")
    print("─" * 50)
    order = ["摄影", "动漫", "插画", "3D", "设计", "艺术", "场景", "游戏", "东方"]
    for cat in order:
        if cat not in by_cat:
            continue
        print(f"\n【{cat}】 {len(by_cat[cat])} 款")
        for name in by_cat[cat]:
            print(f"  • {name}")
    print(
        "\n💡 同义别名示例：anime, ghibli, cyberpunk, genshin, lol, "
        "dunhuang, hanfu, glassmorphism, bauhaus, brutalism, healing, cozy ...\n"
    )


# ─────────────────────────────────────────────────────────
# CLI
# ─────────────────────────────────────────────────────────
def main():
    parser = argparse.ArgumentParser(
        description=f"huo15-img-test v{VERSION} — T2I 提示词增强工具",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
示例:
  # 基础
  enhance_prompt.py "一只赛博朋克风格的猫" -p 赛博朋克 -m Midjourney

  # 自动意图 + 时间 / 天气 / 季节 / 负向需求识别
  enhance_prompt.py "雨天黄昏的东京巷弄，忧郁氛围，不要人物"
  enhance_prompt.py "秋天樱花季汉服写真"

  # 新预设（v2.1）
  enhance_prompt.py "双马尾少女" -p 原神 -t master
  enhance_prompt.py "手持月亮的神女" -p 敦煌壁画
  enhance_prompt.py "极简仪表盘UI" -p 玻璃拟态

  # 角色设定图（给 Midjourney --cref 做参考）
  enhance_prompt.py "银发机甲少女" -p 动漫 --character-sheet -m Midjourney

  # 混合预设（v2.2）
  enhance_prompt.py "持剑女侠" -p "赛博朋克+水墨" --mix 0.6 -m Midjourney
  enhance_prompt.py "山中神女" -p "原神+敦煌壁画" --mix 0.5 -m SDXL

  # 系列一致性（4 张共享 camera/lighting/palette/seed）
  enhance_prompt.py "一个红发女侠" -p 动漫 -s 4 \\
      --variations "持剑站立,骑马奔驰,弯弓射箭,与龙对视"

  # 质量档位 + 显式负面追加
  enhance_prompt.py "品牌展台" -p 品牌KV -t master --avoid "cluttered, people"

  # JSON 输出
  enhance_prompt.py "极简Logo一朵山茶花" -p Logo设计 -j
""",
    )
    parser.add_argument("subject", nargs="?", help="要生成图片的主体描述")
    parser.add_argument(
        "-p", "--preset",
        help="风格预设（中文 / 英文别名）。混合：'赛博朋克+水墨' 或 'genshin+dunhuang'（v2.2）",
    )
    parser.add_argument(
        "--mix", type=float, default=0.6,
        help="主预设权重 0.1-0.9，仅在 -p A+B 混合时生效（默认 0.6，主导主预设）",
    )
    parser.add_argument(
        "-m", "--model", default="通用",
        help="目标模型: Midjourney / SD / SDXL / DALL-E / Flux / 通用",
    )
    parser.add_argument("-a", "--aspect", default="", help="画幅: 1:1 / 3:4 / 16:9 / 21:9 ...")
    parser.add_argument("--mood", default="", help="情绪覆盖")
    parser.add_argument("--composition", default="", help="构图覆盖")
    parser.add_argument("--avoid", default="", help="额外负面词，逗号分隔（v2.1）")
    parser.add_argument(
        "-t", "--tier", choices=["basic", "pro", "master"], default="pro",
        help="质量档位 basic/pro/master，默认 pro（v2.1）",
    )
    parser.add_argument(
        "-cs", "--character-sheet", action="store_true",
        help="角色设定图模式：T-pose 多视图，适合给 MJ --cref 做角色参考（v2.1）",
    )
    parser.add_argument("--seed", type=int, help="种子（不给则哈希生成稳定 seed）")
    parser.add_argument("-s", "--series", type=int, default=1, help="系列张数（配合 --variations 使用）")
    parser.add_argument("--variations", default="", help="系列变体，逗号分隔，如 '持剑,骑马,射箭'")
    parser.add_argument("--polish", action="store_true",
                        help="先用 Claude API 智能润色（需 ANTHROPIC_API_KEY）后再增强（v2.3）")
    parser.add_argument("--safety", default="",
                        help="平台合规润色：DALL-E/MJ/SD/SDXL/Flux，自动重写艺术词避免误判（v2.3）")
    parser.add_argument("-l", "--list", action="store_true", help="列出所有预设")
    parser.add_argument("-j", "--json", action="store_true", help="JSON 输出")
    parser.add_argument("-v", "--version", action="version", version=f"%(prog)s v{VERSION}")

    args = parser.parse_args()

    if args.list:
        list_presets()
        return

    if not args.subject:
        parser.print_help()
        sys.exit(1)

    subject = args.subject
    polish_meta: Optional[Dict] = None
    safety_meta: Optional[Dict] = None
    preset_override = args.preset
    aspect_override = args.aspect
    mix_override = None

    # v2.3: Claude 智能润色（前置）
    if args.polish:
        try:
            sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
            from claude_polish import call_claude, parse_claude_json
            resp = call_claude(subject)
            polished = parse_claude_json(resp)
            if polished.get("error"):
                print(f"❌ Claude 润色拒答: {polished['error']}", file=sys.stderr)
                sys.exit(2)
            subject = polished.get("subject_refined_zh") or subject
            if not preset_override:
                pri = polished.get("style_preset", "")
                sec = polished.get("style_preset_secondary", "")
                if pri and sec:
                    preset_override = f"{pri}+{sec}"
                    mix_override = polished.get("mix_ratio", 0.6)
                elif pri:
                    preset_override = pri
            if not aspect_override and polished.get("aspect"):
                aspect_override = polished["aspect"]
            polish_meta = polished
        except Exception as e:
            print(f"⚠️  Claude 润色失败，回退到原描述: {e}", file=sys.stderr)

    # v2.3: 平台合规润色
    if args.safety:
        try:
            from safety_lint import lint as safety_lint
            r = safety_lint(subject, platform=args.safety)
            if r["verdict"] == "REJECT":
                print(f"🚫 命中红线: {r['reason']}\n类别: {', '.join(r.get('categories', []))}", file=sys.stderr)
                print(r.get("advice", ""), file=sys.stderr)
                sys.exit(2)
            if r["verdict"] == "REWRITE":
                subject = r["rewritten"]
            safety_meta = r
        except ImportError:
            print(f"⚠️  safety_lint 模块未找到", file=sys.stderr)

    # 自动推荐
    auto = parse_requirement(subject)
    raw_preset = preset_override or auto["preset_suggestion"] or "写实摄影"
    primary_raw, secondary_raw = parse_mix_preset(raw_preset)
    preset = primary_raw
    mix_secondary = secondary_raw

    # 校验混合预设
    if mix_secondary:
        primary_resolved = resolve_preset(preset)
        secondary_resolved = resolve_preset(mix_secondary)
        if not primary_resolved or not secondary_resolved:
            unknown = [n for n, r in [(preset, primary_resolved), (mix_secondary, secondary_resolved)] if not r]
            print(f"❌ 未知预设：{', '.join(unknown)}（运行 -l 查看列表）", file=sys.stderr)
            sys.exit(1)
        preset = primary_resolved
        mix_secondary = secondary_resolved

    aspect = aspect_override or auto["aspect_suggestion"] or STYLE_PRESETS.get(resolve_preset(preset) or "写实摄影", {}).get("aspect", "1:1")

    # 混合权重（polish 推荐 > CLI --mix）
    effective_mix = mix_override if mix_override is not None else args.mix

    # 系列模式
    if args.series > 1 or args.variations:
        variations = [v.strip() for v in args.variations.split(",") if v.strip()]
        if not variations:
            variations = [subject] * args.series
        elif len(variations) < args.series:
            variations += [variations[-1]] * (args.series - len(variations))
        results = build_series(
            subject, preset, args.model, aspect,
            variations[: max(args.series, len(variations))],
            seed=args.seed, quality_tier=args.tier,
            mix_secondary=mix_secondary, mix_ratio=effective_mix,
        )
        if args.json:
            out = {"version": VERSION, "series": results}
            if polish_meta: out["claude_polish"] = polish_meta
            if safety_meta: out["safety_lint"] = safety_meta
            print(json.dumps(out, ensure_ascii=False, indent=2))
        else:
            if polish_meta:
                print(f"✨ Claude 已润色 → 主体: {subject}")
            if safety_meta and safety_meta.get("verdict") == "REWRITE":
                print(f"🛡  平台合规重写 → {subject}")
            for r in results:
                print_prompt(r)
            print(f"🔐 本系列 {len(results)} 张共享 seed = {results[0]['seed_suggestion']}，一致性锁见每张「🔒」区块。")
        return

    # 单张
    result = build_prompt(
        subject, preset, args.model, aspect,
        extra_mood=args.mood, extra_composition=args.composition,
        extra_negatives=args.avoid, seed=args.seed,
        quality_tier=args.tier, character_sheet=args.character_sheet,
        mix_secondary=mix_secondary, mix_ratio=effective_mix,
    )
    if polish_meta:
        result["claude_polish"] = polish_meta
    if safety_meta:
        result["safety_lint"] = safety_meta

    if args.json:
        print(json.dumps(result, ensure_ascii=False, indent=2))
    else:
        if polish_meta:
            print(f"✨ Claude 已润色 (in={polish_meta.get('_usage',{}).get('input_tokens',0)}/out={polish_meta.get('_usage',{}).get('output_tokens',0)} tokens)")
        if safety_meta and safety_meta.get("verdict") == "REWRITE":
            print(f"🛡  平台合规已重写: {len(safety_meta.get('substitutions',[]))} 处替换 (target={safety_meta['platform']})")
        print_prompt(result)


if __name__ == "__main__":
    main()


path: scripts/enhance_video.py
content: #!/usr/bin/env python3
"""
huo15-img-test — T2V 视频提示词增强脚本 v2.2

把 enhance_prompt.py 的 88 风格预设 + 一致性锁，扩展到视频维度：
  - 镜头运动（推/拉/摇/移/跟/环绕/手持/无人机...）
  - 节奏（缓慢 / 中速 / 紧张快切）
  - 时长（建议秒数 + 关键帧拆分）
  - 主体动作（自动从描述中抽词，或显式 --action）
  - 模型适配：Sora / Kling 可灵 / Runway Gen-3/Gen-4 / Pika / Luma DreamMachine / 即梦 / Hailuo MiniMax / Wan2.1

调用：
  enhance_video.py "雨夜霓虹街头一只猫漫步" -p 赛博朋克 -m Sora --duration 8
  enhance_video.py "汉服少女转身回眸" -p 汉服写真 -m Kling --motion 慢速跟拍
  enhance_video.py "宇宙飞船穿越星云" -p scifi -m Runway --action "ship accelerates, lens flare"

依赖：
  enhance_prompt.py 同目录（复用其预设 + 意图解析 + 一致性锁）
"""

import sys
import os
import json
import re
import argparse
import hashlib
from typing import Dict, List, Optional, Tuple

sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))

from enhance_prompt import (
    STYLE_PRESETS,
    ALIASES,
    QUALITY_TIERS,
    resolve_preset,
    parse_requirement,
    parse_mix_preset,
    mix_presets,
    sanitize_subject,
    strip_negative_clauses,
    stable_seed,
    list_presets as list_image_presets,
)

VERSION = "2.3.0"

# ─────────────────────────────────────────────────────────
# 镜头运动（中文 → 英文 + 视频专业术语）
# ─────────────────────────────────────────────────────────
CAMERA_MOTION: Dict[str, str] = {
    "推": "slow push-in (dolly in)",
    "推镜": "smooth dolly in, gradual close-up",
    "拉": "pull back (dolly out)",
    "拉镜": "slow pull back revealing wider scene",
    "摇": "pan (horizontal)",
    "横摇": "horizontal pan from left to right",
    "竖摇": "vertical tilt up to down",
    "移": "lateral tracking shot",
    "跟": "tracking shot following the subject",
    "跟拍": "smooth tracking shot, subject locked in frame",
    "环绕": "360 orbital shot around the subject",
    "围绕": "360 orbit shot, slow rotation",
    "手持": "handheld camera, slight shake, documentary feel",
    "稳定": "smooth gimbal stabilized, fluid motion",
    "无人机": "aerial drone shot, high-altitude reveal",
    "航拍": "aerial drone descent, cinematic reveal",
    "升": "crane up, vertical rise",
    "降": "crane down, descent",
    "变焦": "zoom in, focal length change",
    "希区柯克": "dolly zoom (vertigo effect)",
    "希区": "dolly zoom (vertigo effect)",
    "鱼眼": "fisheye lens distortion, wide warped perspective",
    "POV": "first-person POV, immersive",
    "POV视角": "first-person POV, immersive",
    "子弹时间": "bullet-time freeze, 360 frozen pan",
    "延时": "time-lapse, accelerated motion",
    "慢动作": "slow motion 120fps, ultra-smooth",
    "快切": "rapid cuts, high-energy montage",
}

# 节奏 → 英文
PACING: Dict[str, str] = {
    "缓慢": "slow steady pacing, contemplative rhythm",
    "舒缓": "slow steady pacing, contemplative rhythm",
    "宁静": "calm, atmospheric, lingering shots",
    "中速": "moderate pacing, balanced cuts",
    "紧张": "tense pacing, building intensity",
    "急促": "fast pacing, urgent cuts",
    "快切": "rapid cuts, high-energy edit",
    "动感": "kinetic energy, dynamic motion",
    "史诗": "epic crescendo, sweeping movement",
}

# 主体动作关键词（自动抽词）
ACTION_KEYWORDS: Dict[str, str] = {
    "走": "walking forward",
    "漫步": "walking calmly",
    "奔跑": "running fast",
    "跑": "running",
    "跳": "jumping",
    "飞": "flying through the air",
    "舞": "dancing gracefully",
    "舞蹈": "dancing gracefully",
    "回眸": "turning to look back over shoulder",
    "转身": "turning around",
    "微笑": "smiling softly",
    "战斗": "fighting, dynamic combat motion",
    "挥剑": "swinging a sword",
    "射箭": "drawing and releasing an arrow",
    "骑马": "riding a horse at full gallop",
    "驾驶": "driving forward",
    "穿越": "traveling through, breaking forward",
    "升起": "rising up slowly",
    "落下": "falling down gently",
    "爆炸": "explosion blooming outward",
    "绽放": "blooming open",
    "凝视": "gazing intently into the camera",
    "对视": "locking eyes with the viewer",
    "睁眼": "eyes opening slowly",
    "闭眼": "eyes closing slowly",
    "呼吸": "breathing softly, chest rising and falling",
    "拥抱": "embracing tenderly",
    "牵手": "holding hands",
    "握手": "shaking hands",
}

# ─────────────────────────────────────────────────────────
# 模型规格
# ─────────────────────────────────────────────────────────
VIDEO_MODELS: Dict[str, Dict[str, str]] = {
    "Sora": {
        "max_duration": "20s (Sora 2 Pro)",
        "default_duration": 10,
        "aspect_default": "16:9",
        "tip": "支持长自然语言描述。可叠加 'cinematic, IMAX, 35mm film, photorealistic'。一致性强，可复用 character description。",
        "format": "natural",
    },
    "Kling": {
        "max_duration": "10s (1080p Pro)",
        "default_duration": 5,
        "aspect_default": "16:9",
        "tip": "可灵 1.6/2.0：建议提示前置主体，后置镜头/光影。支持首尾帧控制（image-to-video）。",
        "format": "natural",
    },
    "可灵": {
        "max_duration": "10s (1080p Pro)",
        "default_duration": 5,
        "aspect_default": "16:9",
        "tip": "可灵 1.6/2.0：中文提示词支持良好，可加 'cinematic 电影感'。",
        "format": "natural",
    },
    "Runway": {
        "max_duration": "10s (Gen-3 Alpha Turbo)",
        "default_duration": 5,
        "aspect_default": "16:9",
        "tip": "Gen-3 / Gen-4：英文提示效果最佳。支持 Motion Brush 局部运动。CFG ~7。",
        "format": "natural",
    },
    "Pika": {
        "max_duration": "10s (Pika 2.0)",
        "default_duration": 4,
        "aspect_default": "16:9",
        "tip": "Pika：标签式提示，支持 -gs (guidance scale) 和 -motion (1-4)。",
        "format": "tag",
    },
    "Luma": {
        "max_duration": "9s (Dream Machine 1.6)",
        "default_duration": 5,
        "aspect_default": "16:9",
        "tip": "Luma Dream Machine：自然语言 + 关键帧（首尾图）。Loop 模式支持无缝循环。",
        "format": "natural",
    },
    "DreamMachine": {
        "max_duration": "9s",
        "default_duration": 5,
        "aspect_default": "16:9",
        "tip": "Luma Dream Machine：自然语言 + 关键帧。",
        "format": "natural",
    },
    "Hailuo": {
        "max_duration": "10s (MiniMax 02 / S2V-01)",
        "default_duration": 6,
        "aspect_default": "16:9",
        "tip": "海螺 MiniMax 02：中文支持优秀。S2V-01 可指定参考人物。",
        "format": "natural",
    },
    "MiniMax": {
        "max_duration": "10s",
        "default_duration": 6,
        "aspect_default": "16:9",
        "tip": "MiniMax 视频：中英双语，长描述效果好。",
        "format": "natural",
    },
    "即梦": {
        "max_duration": "12s (Seedance 1.0)",
        "default_duration": 5,
        "aspect_default": "16:9",
        "tip": "即梦 / Seedance：抖音生态，支持中文 + 多镜头剧情连贯。",
        "format": "natural",
    },
    "Seedance": {
        "max_duration": "12s",
        "default_duration": 5,
        "aspect_default": "16:9",
        "tip": "Seedance 1.0：多镜头剧情连贯，支持中文。",
        "format": "natural",
    },
    "Wan": {
        "max_duration": "8s (Wan 2.1)",
        "default_duration": 4,
        "aspect_default": "16:9",
        "tip": "通义 Wan 2.1：阿里开源，I2V 支持高分辨率。中英双语提示。",
        "format": "natural",
    },
    "Wan2.1": {
        "max_duration": "8s",
        "default_duration": 4,
        "aspect_default": "16:9",
        "tip": "通义 Wan 2.1：阿里开源 14B / 1.3B 双参数。",
        "format": "natural",
    },
    "通用": {
        "max_duration": "—",
        "default_duration": 5,
        "aspect_default": "16:9",
        "tip": "通用模板：自然语言 + 镜头 + 节奏 + 主体动作。",
        "format": "natural",
    },
}

MODEL_ALIASES: Dict[str, str] = {
    "sora": "Sora", "kling": "Kling", "kelin": "Kling", "klingai": "Kling",
    "runway": "Runway", "gen3": "Runway", "gen4": "Runway",
    "pika": "Pika", "luma": "Luma", "dreammachine": "Luma",
    "hailuo": "Hailuo", "minimax": "Hailuo",
    "jimeng": "即梦", "seedance": "即梦",
    "wan": "Wan", "wan21": "Wan", "wan2.1": "Wan",
    "tongyi": "Wan",
}


def resolve_video_model(name: str) -> str:
    if not name:
        return "通用"
    key = name.strip().lower().replace("-", "").replace("_", "").replace(" ", "")
    if key in MODEL_ALIASES:
        return MODEL_ALIASES[key]
    for m in VIDEO_MODELS:
        if m.lower() == key:
            return m
    return name if name in VIDEO_MODELS else "通用"


# ─────────────────────────────────────────────────────────
# 解析
# ─────────────────────────────────────────────────────────
def parse_motion(text: str) -> str:
    for zh, en in CAMERA_MOTION.items():
        if zh in text:
            return en
    return ""


def parse_pacing(text: str) -> str:
    for zh, en in PACING.items():
        if zh in text:
            return en
    return ""


def parse_action(text: str) -> str:
    actions = []
    for zh, en in ACTION_KEYWORDS.items():
        if zh in text and en not in actions:
            actions.append(en)
    return ", ".join(actions[:3])


# ─────────────────────────────────────────────────────────
# 关键帧拆分
# ─────────────────────────────────────────────────────────
def keyframe_breakdown(subject: str, motion: str, duration: int) -> List[Dict[str, str]]:
    """简单的三段式拆分：开场（建立）→ 中段（动作）→ 结尾（落点）。"""
    if duration <= 3:
        return [{"t": "0s", "desc": f"establish shot: {subject}"}]
    third = max(1, duration // 3)
    return [
        {"t": "0s", "desc": f"opening: establish {subject} in scene, static composition"},
        {"t": f"{third}s", "desc": f"mid: {motion or 'subject performs main action'}, peak motion"},
        {"t": f"{2*third}s", "desc": f"closing: settle into resting frame, fade or hold"},
    ]


# ─────────────────────────────────────────────────────────
# 主构建
# ─────────────────────────────────────────────────────────
def build_video_prompt(
    subject: str,
    preset: str,
    model: str = "通用",
    aspect: str = "",
    duration: Optional[int] = None,
    motion: str = "",
    pacing: str = "",
    action: str = "",
    seed: Optional[int] = None,
    quality_tier: str = "pro",
    extra_negatives: str = "",
    mix_secondary: Optional[str] = None,
    mix_ratio: float = 0.6,
) -> Dict:
    preset = resolve_preset(preset) or "电影感"
    if mix_secondary:
        mix_secondary = resolve_preset(mix_secondary) or ""
    model = resolve_video_model(model)
    spec = VIDEO_MODELS[model]

    # 视觉锁（复用 image preset）
    if mix_secondary and mix_secondary != preset:
        data = mix_presets(preset, mix_secondary, mix_ratio, model)
        mixed_label = f"{preset}+{mix_secondary}@{mix_ratio:.2f}"
    else:
        data = STYLE_PRESETS[preset]
        mixed_label = ""

    # 时长 / 画幅
    if duration is None:
        duration = spec["default_duration"]
    if not aspect:
        aspect = data.get("aspect", spec["aspect_default"])

    # 自动解析
    auto = parse_requirement(subject)
    subject_clean = sanitize_subject(strip_negative_clauses(subject))
    if not motion:
        motion = parse_motion(subject) or "smooth gimbal stabilized, fluid motion"
    if not pacing:
        pacing = parse_pacing(subject) or "moderate pacing, balanced cuts"
    if not action:
        action = parse_action(subject)

    # ambient
    ambient_parts = [auto["time_of_day"], auto["weather"], auto["season"]]
    ambient = ", ".join([x for x in ambient_parts if x])

    # 视觉锁字段
    visual_lock = ", ".join([
        x for x in [data["tags"], data.get("camera", ""), data.get("lighting", ""), data.get("palette", "")] if x
    ])

    quality_phrase = QUALITY_TIERS.get(quality_tier, QUALITY_TIERS["pro"])

    seed_key = mixed_label or preset
    seed_value = seed or stable_seed(subject_clean, seed_key)

    # 构造正向提示
    if spec["format"] == "tag":  # Pika 标签格式
        parts = [
            subject_clean,
            f"{motion}",
            f"{pacing}",
            visual_lock,
            ambient,
            action,
            quality_phrase,
            "cinematic video",
        ]
        positive = ", ".join([p for p in parts if p])
        positive += f" -gs 12 -motion 3 -ar {aspect}"
    else:  # 自然语言格式
        sentences = []
        sentences.append(f"A {duration}-second video of {subject_clean}.")
        sentences.append(f"Camera movement: {motion}.")
        if action:
            sentences.append(f"The subject is {action}.")
        sentences.append(f"Pacing: {pacing}.")
        sentences.append(f"Visual style: {visual_lock}.")
        if ambient:
            sentences.append(f"Atmosphere: {ambient}.")
        sentences.append(f"Quality: {quality_phrase}, cinematic, smooth temporal coherence, no flicker, consistent character across frames.")
        positive = " ".join(sentences)

    # 负面
    base_neg = data["neg"]
    video_neg = (
        "flicker, frame drop, motion blur artifacts, jittery camera, "
        "low fps, choppy motion, morphing artifacts, identity drift, "
        "deformed limbs mid-motion, inconsistent character, watermark"
    )
    neg_parts = [base_neg, video_neg, extra_negatives, ", ".join(auto.get("user_negatives", []))]
    negative = ", ".join([x for x in neg_parts if x])

    # 关键帧
    keyframes = keyframe_breakdown(subject_clean, motion, duration)

    hint = (
        f"{model} tips：\n"
        f"  • {spec['tip']}\n"
        f"  • 推荐时长：{duration}s（上限 {spec['max_duration']}）\n"
        f"  • 一致性：i2v 模式可固定首帧角色 / 用 image-prompt 保持服装色彩\n"
        f"  • seed: {seed_value}（同一 seed + 同一 prompt 在多数模型可复现）"
    )

    return {
        "version": VERSION,
        "type": "t2v",
        "original": subject,
        "preset": preset,
        "mix_secondary": mix_secondary or "",
        "mix_label": mixed_label,
        "model": model,
        "aspect": aspect,
        "duration_s": duration,
        "max_duration": spec["max_duration"],
        "motion": motion,
        "pacing": pacing,
        "action": action,
        "time_of_day": auto.get("time_of_day", ""),
        "weather": auto.get("weather", ""),
        "season": auto.get("season", ""),
        "seed_suggestion": seed_value,
        "quality_tier": quality_tier,
        "positive": positive,
        "negative": negative,
        "keyframes": keyframes,
        "hint": hint,
        "consistency_lock": {
            "camera": data.get("camera", ""),
            "lighting": data.get("lighting", ""),
            "palette": data.get("palette", ""),
            "aspect": aspect,
            "motion": motion,
        },
    }


def print_video_prompt(r: Dict):
    sep = "═" * 60
    print(f"\n{sep}")
    print(f"🎬 视频提示词（v{r['version']}）")
    print(f"📌 原始描述   : {r['original']}")
    if r.get("mix_label"):
        print(f"🎨 风格预设   : {r['mix_label']} (混合)")
    else:
        print(f"🎨 风格预设   : {r['preset']}")
    print(f"🤖 目标模型   : {r['model']}（上限 {r['max_duration']}）")
    print(f"📐 画幅       : {r['aspect']}")
    print(f"⏱  时长       : {r['duration_s']}s")
    print(f"🎥 镜头运动   : {r['motion']}")
    print(f"🎵 节奏       : {r['pacing']}")
    if r.get("action"):
        print(f"💪 主体动作   : {r['action']}")
    if r.get("time_of_day") or r.get("weather") or r.get("season"):
        amb = ", ".join([x for x in [r.get("time_of_day", ""), r.get("weather", ""), r.get("season", "")] if x])
        print(f"🌤  环境       : {amb}")
    print(f"⭐ 质量档位   : {r['quality_tier']}")
    print(f"🎲 种子建议   : {r['seed_suggestion']}")
    print(f"\n✅ 正向提示词：\n{r['positive']}")
    print(f"\n❌ 负向提示词：\n{r['negative']}")
    print(f"\n🎞  关键帧拆分：")
    for kf in r["keyframes"]:
        print(f"   {kf['t']:>4s}  {kf['desc']}")
    print(f"\n🔒 一致性锁：")
    for k, v in r["consistency_lock"].items():
        if v:
            print(f"   {k:8s}: {v}")
    print(f"\n💡 {r['hint']}")
    print(f"{sep}\n")


def main():
    parser = argparse.ArgumentParser(
        description=f"huo15-img-test enhance_video v{VERSION} — T2V 视频提示词增强",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
示例:
  enhance_video.py "雨夜霓虹街头一只猫漫步" -p 赛博朋克 -m Sora --duration 8
  enhance_video.py "汉服少女转身回眸" -p 汉服写真 -m Kling --motion 慢速跟拍
  enhance_video.py "宇宙飞船穿越星云" -p scifi -m Runway --duration 5 --pacing 史诗
  enhance_video.py "山中神女腾云" -p "原神+敦煌壁画" --mix 0.6 -m Hailuo
  enhance_video.py "侠客挥剑" -p 水墨 -m 即梦 --action "spinning sword strike"
""",
    )
    parser.add_argument("subject", nargs="?", help="主体描述")
    parser.add_argument("-p", "--preset", help="风格预设（沿用 88 款图像预设；支持 A+B 混合）")
    parser.add_argument("--mix", type=float, default=0.6, help="主预设权重 0.1-0.9（默认 0.6）")
    parser.add_argument(
        "-m", "--model", default="通用",
        help="视频模型: Sora / Kling / Runway / Pika / Luma / Hailuo / 即梦 / Wan / 通用",
    )
    parser.add_argument("-a", "--aspect", default="", help="画幅 16:9 / 9:16 / 1:1 / 21:9")
    parser.add_argument("--duration", type=int, help="时长（秒），不给走模型默认")
    parser.add_argument("--motion", default="", help="镜头运动覆盖（中/英）")
    parser.add_argument("--pacing", default="", help="节奏覆盖")
    parser.add_argument("--action", default="", help="主体动作覆盖")
    parser.add_argument("--avoid", default="", help="额外负面词")
    parser.add_argument("-t", "--tier", choices=["basic", "pro", "master"], default="pro")
    parser.add_argument("--seed", type=int, help="种子")
    parser.add_argument("-l", "--list", action="store_true", help="列出图像预设（视频沿用）")
    parser.add_argument("--list-models", action="store_true", help="列出视频模型规格")
    parser.add_argument("-j", "--json", action="store_true", help="JSON 输出")
    parser.add_argument("-v", "--version", action="version", version=f"%(prog)s v{VERSION}")
    args = parser.parse_args()

    if args.list:
        list_image_presets()
        return

    if args.list_models:
        print(f"\n🎬 视频模型规格 (v{VERSION})\n" + "─" * 50)
        for name, spec in VIDEO_MODELS.items():
            print(f"\n【{name}】")
            print(f"  上限时长: {spec['max_duration']}")
            print(f"  默认时长: {spec['default_duration']}s")
            print(f"  默认画幅: {spec['aspect_default']}")
            print(f"  说明: {spec['tip']}")
        return

    if not args.subject:
        parser.print_help()
        sys.exit(1)

    raw_preset = args.preset or "电影感"
    primary_raw, secondary_raw = parse_mix_preset(raw_preset)
    if secondary_raw:
        primary_resolved = resolve_preset(primary_raw)
        secondary_resolved = resolve_preset(secondary_raw)
        if not primary_resolved or not secondary_resolved:
            unknown = [n for n, r in [(primary_raw, primary_resolved), (secondary_raw, secondary_resolved)] if not r]
            print(f"❌ 未知预设：{', '.join(unknown)}", file=sys.stderr)
            sys.exit(1)
        preset, mix_secondary = primary_resolved, secondary_resolved
    else:
        preset, mix_secondary = primary_raw, None

    result = build_video_prompt(
        args.subject, preset, model=args.model, aspect=args.aspect,
        duration=args.duration, motion=args.motion, pacing=args.pacing,
        action=args.action, seed=args.seed, quality_tier=args.tier,
        extra_negatives=args.avoid, mix_secondary=mix_secondary, mix_ratio=args.mix,
    )

    if args.json:
        print(json.dumps(result, ensure_ascii=False, indent=2))
    else:
        print_video_prompt(result)


if __name__ == "__main__":
    main()


path: scripts/render_prompt.py
content: #!/usr/bin/env python3
"""
huo15-img-test — 提示词直出图片 v2.2

把 enhance_prompt.py 生成的提示词，直接调用本地或云端 API 出图。

支持的后端：
  - comfyui     本地 ComfyUI（HTTP API，默认 http://127.0.0.1:8188）
  - sd-webui    AUTOMATIC1111 / Forge（默认 http://127.0.0.1:7860/sdapi/v1/txt2img）
  - dalle       OpenAI DALL-E 3（OPENAI_API_KEY）
  - openai      同 dalle
  - none        只生成调用脚本，不真实执行（dry-run，方便贴到 ComfyUI 桌面端）

依赖：仅 Python 标准库（urllib），不引入 requests/PIL，避免企业扫描器命中第三方包。

调用：
  render_prompt.py "赛博朋克猫" -p 赛博朋克 -m SD --backend sd-webui
  render_prompt.py "极简logo" -p Logo设计 --backend dalle --size 1024x1024
  render_prompt.py "原神少女" -p 原神 --backend comfyui --workflow ./workflows/sdxl-base.json
  render_prompt.py "敦煌神女" -p 敦煌壁画 --backend none -j > recipe.json   # dry-run

环境变量：
  OPENAI_API_KEY        DALL-E 调用必需
  COMFYUI_URL           覆盖 ComfyUI 端点（默认 http://127.0.0.1:8188）
  SDWEBUI_URL           覆盖 SD WebUI 端点（默认 http://127.0.0.1:7860）
"""

import sys
import os
import json
import time
import base64
import argparse
import uuid
from typing import Dict, Optional
from urllib.parse import urljoin
from urllib.request import Request, urlopen
from urllib.error import HTTPError, URLError

sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))

from enhance_prompt import (
    build_prompt,
    parse_mix_preset,
    resolve_preset,
    parse_requirement,
    STYLE_PRESETS,
    ASPECT_TO_SDXL,
)

VERSION = "2.3.0"


# ─────────────────────────────────────────────────────────
# HTTP 工具
# ─────────────────────────────────────────────────────────
def http_post_json(url: str, body: Dict, headers: Optional[Dict] = None, timeout: int = 600) -> Dict:
    data = json.dumps(body).encode("utf-8")
    h = {"Content-Type": "application/json"}
    if headers:
        h.update(headers)
    req = Request(url, data=data, headers=h, method="POST")
    with urlopen(req, timeout=timeout) as r:
        return json.loads(r.read().decode("utf-8"))


def http_get_json(url: str, headers: Optional[Dict] = None, timeout: int = 60) -> Dict:
    req = Request(url, headers=headers or {})
    with urlopen(req, timeout=timeout) as r:
        return json.loads(r.read().decode("utf-8"))


def http_get_bytes(url: str, headers: Optional[Dict] = None, timeout: int = 600) -> bytes:
    req = Request(url, headers=headers or {})
    with urlopen(req, timeout=timeout) as r:
        return r.read()


# ─────────────────────────────────────────────────────────
# DALL-E 3
# ─────────────────────────────────────────────────────────
DALLE_SIZES = {"1:1": "1024x1024", "16:9": "1792x1024", "9:16": "1024x1792"}


def render_dalle(positive: str, size: str, output_dir: str, n: int = 1) -> Dict:
    [REDACTED_SECRET]"OPENAI_API_KEY")
    if not api_key:
        raise RuntimeError("缺少 OPENAI_API_KEY 环境变量")
    body = {
        "model": "dall-e-3",
        "prompt": positive[:4000],
        "n": n,
        "size": size,
        "quality": "hd",
        "response_format": "b64_json",
    }
    resp = http_post_json(
        "https://api.openai.com/v1/images/generations",
        body,
        headers={"Authorization": f"Bearer {api_key}"},
        timeout=300,
    )
    saved = []
    os.makedirs(output_dir, exist_ok=True)
    for i, item in enumerate(resp.get("data", [])):
        path = os.path.join(output_dir, f"dalle-{int(time.time())}-{i}.png")
        with open(path, "wb") as f:
            f.write(base64.b64decode(item["b64_json"]))
        saved.append(path)
    return {"backend": "dalle", "saved": saved, "raw_response_keys": list(resp.keys())}


# ─────────────────────────────────────────────────────────
# AUTOMATIC1111 / Forge SD WebUI
# ─────────────────────────────────────────────────────────
def aspect_to_size(aspect: str) -> tuple:
    sdxl = ASPECT_TO_SDXL.get(aspect, "1024x1024")
    w, h = sdxl.split("x")
    return int(w), int(h)


def render_sdwebui(positive: str, negative: str, aspect: str, seed: int, steps: int, cfg: float,
                   sampler: str, output_dir: str, base_url: Optional[str] = None) -> Dict:
    base = base_url or os.environ.get("SDWEBUI_URL", "http://127.0.0.1:7860")
    w, h = aspect_to_size(aspect)
    body = {
        "prompt": positive,
        "negative_prompt": negative,
        "width": w,
        "height": h,
        "seed": seed,
        "steps": steps,
        "cfg_scale": cfg,
        "sampler_name": sampler,
        "send_images": True,
    }
    resp = http_post_json(urljoin(base, "/sdapi/v1/txt2img"), body, timeout=900)
    saved = []
    os.makedirs(output_dir, exist_ok=True)
    for i, b64 in enumerate(resp.get("images", [])):
        path = os.path.join(output_dir, f"sdwebui-{seed}-{i}.png")
        with open(path, "wb") as f:
            f.write(base64.b64decode(b64.split(",", 1)[-1]))
        saved.append(path)
    return {"backend": "sd-webui", "saved": saved, "info": resp.get("info", "")[:200]}


# ─────────────────────────────────────────────────────────
# ComfyUI
# ─────────────────────────────────────────────────────────
DEFAULT_COMFY_WORKFLOW = {
    "3": {
        "class_type": "KSampler",
        "inputs": {
            "seed": 0, "steps": 25, "cfg": 7.0, "sampler_name": "dpmpp_2m",
            "scheduler": "karras", "denoise": 1.0,
            "model": ["4", 0], "positive": ["6", 0], "negative": ["7", 0], "latent_image": ["5", 0],
        },
    },
    "4": {"class_type": "CheckpointLoaderSimple", "inputs": {"ckpt_name": "sd_xl_base_1.0.safetensors"}},
    "5": {"class_type": "EmptyLatentImage", "inputs": {"width": 1024, "height": 1024, "batch_size": 1}},
    "6": {"class_type": "CLIPTextEncode", "inputs": {"text": "POSITIVE_PLACEHOLDER", "clip": ["4", 1]}},
    "7": {"class_type": "CLIPTextEncode", "inputs": {"text": "NEGATIVE_PLACEHOLDER", "clip": ["4", 1]}},
    "8": {"class_type": "VAEDecode", "inputs": {"samples": ["3", 0], "vae": ["4", 2]}},
    "9": {"class_type": "SaveImage", "inputs": {"images": ["8", 0], "filename_prefix": "huo15"}},
}


def render_comfyui(positive: str, negative: str, aspect: str, seed: int, steps: int, cfg: float,
                   workflow_path: Optional[str], output_dir: str,
                   base_url: Optional[str] = None) -> Dict:
    base = base_url or os.environ.get("COMFYUI_URL", "http://127.0.0.1:8188")

    if workflow_path and os.path.isfile(workflow_path):
        with open(workflow_path, "r", encoding="utf-8") as f:
            workflow = json.load(f)
    else:
        workflow = json.loads(json.dumps(DEFAULT_COMFY_WORKFLOW))

    w, h = aspect_to_size(aspect)
    for node in workflow.values():
        ct = node.get("class_type", "")
        ins = node.get("inputs", {})
        if ct == "CLIPTextEncode":
            if ins.get("text") == "POSITIVE_PLACEHOLDER" or "positive" in str(ins.get("text", "")).lower():
                ins["text"] = positive
            elif ins.get("text") == "NEGATIVE_PLACEHOLDER" or "negative" in str(ins.get("text", "")).lower():
                ins["text"] = negative
        elif ct == "EmptyLatentImage":
            ins["width"], ins["height"] = w, h
        elif ct == "KSampler":
            ins["seed"], ins["steps"], ins["cfg"] = seed, steps, cfg

    pos_set = neg_set = False
    for node in workflow.values():
        if node.get("class_type") == "CLIPTextEncode":
            if not pos_set:
                node["inputs"]["text"] = positive
                pos_set = True
            elif not neg_set:
                node["inputs"]["text"] = negative
                neg_set = True

    client_id = str(uuid.uuid4())
    queue_resp = http_post_json(urljoin(base, "/prompt"), {"prompt": workflow, "client_id": client_id}, timeout=30)
    prompt_id = queue_resp.get("prompt_id")
    if not prompt_id:
        raise RuntimeError(f"ComfyUI 队列失败: {queue_resp}")

    deadline = time.time() + 600
    history = {}
    while time.time() < deadline:
        try:
            history = http_get_json(urljoin(base, f"/history/{prompt_id}"), timeout=10)
            if history.get(prompt_id):
                break
        except (HTTPError, URLError):
            pass
        time.sleep(2)

    if not history.get(prompt_id):
        raise RuntimeError("ComfyUI 任务超时")

    saved = []
    os.makedirs(output_dir, exist_ok=True)
    outputs = history[prompt_id].get("outputs", {})
    for node_id, output in outputs.items():
        for img in output.get("images", []):
            url = urljoin(base, f"/view?filename={img['filename']}&subfolder={img.get('subfolder','')}&type={img.get('type','output')}")
            path = os.path.join(output_dir, f"comfy-{seed}-{img['filename']}")
            with open(path, "wb") as f:
                f.write(http_get_bytes(url))
            saved.append(path)

    return {"backend": "comfyui", "saved": saved, "prompt_id": prompt_id}


# ─────────────────────────────────────────────────────────
# 主入口
# ─────────────────────────────────────────────────────────
def main():
    parser = argparse.ArgumentParser(
        description=f"huo15-img-test render_prompt v{VERSION} — 提示词直出图片",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
示例:
  render_prompt.py "赛博朋克猫" -p 赛博朋克 --backend sd-webui
  render_prompt.py "原神少女" -p 原神 --backend comfyui --workflow ./workflows/sdxl.json
  render_prompt.py "极简logo" -p Logo设计 --backend dalle --size 1024x1024
  render_prompt.py "敦煌神女" -p 敦煌壁画 --backend none -j   # dry-run，只输出 recipe
""",
    )
    parser.add_argument("subject", help="主体描述")
    parser.add_argument("-p", "--preset", help="风格预设（支持 A+B 混合）")
    parser.add_argument("--mix", type=float, default=0.6, help="混合权重（默认 0.6）")
    parser.add_argument("-a", "--aspect", default="", help="画幅")
    parser.add_argument("-t", "--tier", choices=["basic", "pro", "master"], default="pro")
    parser.add_argument("--avoid", default="", help="额外负面词")
    parser.add_argument("--seed", type=int, help="种子")
    parser.add_argument(
        "--backend", choices=["comfyui", "sd-webui", "dalle", "openai", "none"], default="none",
        help="后端：comfyui / sd-webui / dalle / none(dry-run)",
    )
    parser.add_argument("-m", "--model", default="SDXL", help="提示词模型适配（不影响后端选择）")
    parser.add_argument("--output", default="./renders", help="输出目录（默认 ./renders）")
    parser.add_argument("--workflow", default="", help="ComfyUI workflow JSON 路径（可选）")
    parser.add_argument("--steps", type=int, default=25, help="采样步数")
    parser.add_argument("--cfg", type=float, default=7.0, help="CFG scale")
    parser.add_argument("--sampler", default="DPM++ 2M Karras", help="采样器")
    parser.add_argument("--size", default="", help="DALL-E 尺寸 1024x1024 / 1792x1024 / 1024x1792")
    parser.add_argument("--n", type=int, default=1, help="生成张数（DALL-E）")
    parser.add_argument("-j", "--json", action="store_true", help="JSON 输出")
    parser.add_argument("-v", "--version", action="version", version=f"%(prog)s v{VERSION}")
    args = parser.parse_args()

    raw_preset = args.preset or "写实摄影"
    primary_raw, secondary_raw = parse_mix_preset(raw_preset)
    if secondary_raw:
        primary_resolved = resolve_preset(primary_raw)
        secondary_resolved = resolve_preset(secondary_raw)
        if not primary_resolved or not secondary_resolved:
            print(f"❌ 未知预设：{primary_raw} 或 {secondary_raw}", file=sys.stderr)
            sys.exit(1)
        preset, mix_secondary = primary_resolved, secondary_resolved
    else:
        preset, mix_secondary = primary_raw, None

    auto = parse_requirement(args.subject)
    aspect = args.aspect or auto["aspect_suggestion"] or STYLE_PRESETS.get(resolve_preset(preset) or "写实摄影", {}).get("aspect", "1:1")

    recipe = build_prompt(
        args.subject, preset, args.model, aspect,
        extra_negatives=args.avoid, seed=args.seed, quality_tier=args.tier,
        mix_secondary=mix_secondary, mix_ratio=args.mix,
    )
    seed = recipe["seed_suggestion"]

    if args.backend == "none":
        out = {"version": VERSION, "backend": "none", "recipe": recipe, "note": "dry-run，未实际调用模型"}
        if args.json:
            print(json.dumps(out, ensure_ascii=False, indent=2))
        else:
            print(f"🧪 dry-run（未出图）")
            print(f"  positive: {recipe['positive'][:200]}...")
            print(f"  seed:     {seed}")
            print(f"  → 用 -j 输出完整 recipe，再 pipe 给 ComfyUI / DALL-E / SD WebUI")
        return

    try:
        if args.backend == "sd-webui":
            result = render_sdwebui(
                recipe["positive"], recipe["negative"], aspect, seed,
                args.steps, args.cfg, args.sampler, args.output,
            )
        elif args.backend == "comfyui":
            result = render_comfyui(
                recipe["positive"], recipe["negative"], aspect, seed,
                args.steps, args.cfg, args.workflow or None, args.output,
            )
        elif args.backend in ("dalle", "openai"):
            size = args.size or DALLE_SIZES.get(aspect, "1024x1024")
            result = render_dalle(recipe["positive"], size, args.output, n=args.n)
        else:
            raise RuntimeError(f"未知 backend: {args.backend}")
    except Exception as e:
        print(f"❌ 渲染失败: {e}", file=sys.stderr)
        sys.exit(2)

    out = {"version": VERSION, "recipe": recipe, "render": result}
    if args.json:
        print(json.dumps(out, ensure_ascii=False, indent=2))
    else:
        print(f"✅ 已出图（backend={result['backend']}）")
        for p in result.get("saved", []):
            print(f"   📷 {p}")
        print(f"   🎲 seed = {seed}")


if __name__ == "__main__":
    main()


path: scripts/reverse_prompt.py
content: #!/usr/bin/env python3
"""
huo15-img-test — 参考图反解 v2.2

把现成图片（本地路径或 URL）反向解析成可复用的 T2I 提示词。

工作流（三层）：
1. PNG metadata 提取：A1111 / ComfyUI / NovelAI 出图都把 prompt 写在 PNG `parameters` / `prompt` / `Comment` 字段
2. EXIF 提取：iPhone / 单反相机参数（焦距 / ISO / 快门 / 光圈），用于推断 camera 锁
3. VLM 模板生成：当 1/2 都没有可用信息时，输出标准化「请把这张图描述成 T2I 提示词」prompt 模板，
   交给 GPT-4o / Claude / Gemini 1.5 / Qwen-VL 等多模态模型继续解析

输出三选一：
  - text  人类可读
  - json  结构化（直接喂回 enhance_prompt.py）
  - mj    Midjourney 风格直接复用 prompt（含 --ar / --sref / --seed）

调用：
  reverse_prompt.py /path/to/image.png
  reverse_prompt.py https://example.com/img.png --vlm
  reverse_prompt.py img.png -j > recipe.json && enhance_prompt.py "$(jq -r .subject recipe.json)"
"""

import sys
import os
import json
import re
import argparse
import struct
from typing import Dict, List, Optional, Tuple
from urllib.parse import urlparse
from urllib.request import Request, urlopen

VERSION = "2.3.0"

# ─────────────────────────────────────────────────────────
# PNG metadata 解析
# ─────────────────────────────────────────────────────────
PNG_TEXT_KEYS_A1111 = ("parameters",)
PNG_TEXT_KEYS_COMFY = ("prompt", "workflow")
PNG_TEXT_KEYS_NOVELAI = ("Description", "Comment", "Software")


def read_png_text_chunks(blob: bytes) -> Dict[str, str]:
    """手写 PNG 解析，避免 PIL 依赖。提取 tEXt / iTXt / zTXt 文本块。"""
    if not blob.startswith(b"\x89PNG\r\n\x1a\n"):
        return {}
    out: Dict[str, str] = {}
    i = 8
    while i < len(blob):
        if i + 8 > len(blob):
            break
        length = struct.unpack(">I", blob[i:i+4])[0]
        ctype = blob[i+4:i+8]
        data = blob[i+8:i+8+length]
        i += 8 + length + 4  # skip CRC
        if ctype == b"tEXt":
            try:
                key, value = data.split(b"\x00", 1)
                out[key.decode("latin-1", "replace")] = value.decode("utf-8", "replace")
            except ValueError:
                continue
        elif ctype == b"iTXt":
            try:
                key, rest = data.split(b"\x00", 1)
                # iTXt: key\0 compress_flag(1) compress_method(1) lang_tag\0 trans_keyword\0 text
                if len(rest) < 2:
                    continue
                _flag, _method = rest[0], rest[1]
                rest2 = rest[2:]
                _lang, rest3 = rest2.split(b"\x00", 1)
                _trans, text = rest3.split(b"\x00", 1)
                out[key.decode("latin-1", "replace")] = text.decode("utf-8", "replace")
            except (ValueError, IndexError):
                continue
        elif ctype == b"IEND":
            break
    return out


def parse_a1111_params(text: str) -> Dict[str, str]:
    """解析 AUTOMATIC1111 / ForgeUI 的 parameters 文本。

    格式：
        positive_prompt
        Negative prompt: ...
        Steps: 30, Sampler: ..., CFG scale: ..., Seed: ..., Size: ..., Model: ...
    """
    out: Dict[str, str] = {}
    if "Negative prompt:" in text:
        pos, rest = text.split("Negative prompt:", 1)
        out["positive"] = pos.strip()
        if "\n" in rest:
            neg, params = rest.split("\n", 1)
            out["negative"] = neg.strip()
        else:
            params = ""
            out["negative"] = rest.strip()
    else:
        if "\n" in text and re.search(r"^\w+:", text.strip().split("\n")[-1]):
            lines = text.strip().split("\n")
            out["positive"] = "\n".join(lines[:-1]).strip()
            params = lines[-1]
        else:
            out["positive"] = text.strip()
            params = ""

    for kv in re.findall(r"([A-Za-z][\w\s]*?):\s*([^,]+)", params):
        k, v = kv[0].strip().lower().replace(" ", "_"), kv[1].strip()
        out[k] = v
    return out


def detect_source(meta: Dict[str, str]) -> str:
    if "parameters" in meta:
        return "a1111"
    if "prompt" in meta and "workflow" in meta:
        return "comfyui"
    if any(k in meta for k in ("Description", "Software")) and "Comment" in meta:
        return "novelai"
    if any("Stable Diffusion" in str(v) for v in meta.values()):
        return "sd-generic"
    return "unknown"


# ─────────────────────────────────────────────────────────
# 启发式：从 prompt 文本推断风格预设
# ─────────────────────────────────────────────────────────
PRESET_HEURISTICS: List[Tuple[str, str]] = [
    (r"\b(cyberpunk|neon|blade runner|holographic)\b", "赛博朋克"),
    (r"\b(steampunk|brass|gears)\b", "蒸汽朋克"),
    (r"\b(ghibli|miyazaki|studio ghibli)\b", "宫崎骏"),
    (r"\b(makoto shinkai|shinkai)\b", "新海诚"),
    (r"\b(genshin|mihoyo|honkai)\b", "原神"),
    (r"\b(dunhuang|tang dynasty fresco|apsara)\b", "敦煌壁画"),
    (r"\b(hanfu)\b", "汉服写真"),
    (r"\b(ink wash|sumi-e|chinese ink)\b", "水墨"),
    (r"\b(ukiyo-e|woodblock)\b", "浮世绘"),
    (r"\b(glassmorphism|frosted glass)\b", "玻璃拟态"),
    (r"\b(neumorphism|soft ui)\b", "新拟态"),
    (r"\b(bauhaus)\b", "包豪斯"),
    (r"\b(brutalism|brutalist concrete)\b", "粗野主义"),
    (r"\b(wabi[\s-]?sabi)\b", "侘寂"),
    (r"\b(film grain|kodak|portra|analog film)\b", "胶片摄影"),
    (r"\b(black and white|monochrome|silver gelatin)\b", "黑白摄影"),
    (r"\b(low poly|lowpoly)\b", "低多边形"),
    (r"\b(isometric)\b", "等距视图"),
    (r"\b(claymation|clay)\b", "粘土"),
    (r"\b(impressionist|monet|renoir)\b", "印象派"),
    (r"\b(van gogh|post impressionist)\b", "后印象派"),
    (r"\b(art deco|gatsby)\b", "装饰艺术"),
    (r"\b(art nouveau|mucha)\b", "新艺术"),
    (r"\b(vaporwave|y2k)\b", "Vaporwave"),
    (r"\b(anime|cel shaded|cel-shaded)\b", "动漫"),
    (r"\b(watercolor)\b", "水彩"),
    (r"\b(oil painting)\b", "油画"),
    (r"\b(pixel art|8[\s-]?bit|16[\s-]?bit)\b", "像素艺术"),
    (r"\b(minimalist|minimal)\b", "极简主义"),
    (r"\b(cinematic|imax|35mm)\b", "电影感"),
    (r"\b(concept art)\b", "概念艺术"),
    (r"\b(dark fantasy)\b", "黑暗奇幻"),
    (r"\b(fantasy|epic fantasy)\b", "奇幻"),
    (r"\b(sci[\s-]?fi|space opera)\b", "科幻"),
]


def guess_preset(positive: str) -> str:
    p = positive.lower()
    for pattern, preset in PRESET_HEURISTICS:
        if re.search(pattern, p):
            return preset
    return ""


def guess_aspect(size_str: str) -> str:
    if not size_str or "x" not in size_str.lower():
        return ""
    try:
        w, h = [int(x) for x in re.findall(r"\d+", size_str)[:2]]
    except (ValueError, IndexError):
        return ""
    ratio = w / h if h else 1
    candidates = [
        ("1:1", 1.0), ("16:9", 16/9), ("9:16", 9/16),
        ("3:4", 3/4), ("4:3", 4/3), ("21:9", 21/9), ("3:2", 3/2), ("2:3", 2/3),
    ]
    return min(candidates, key=lambda c: abs(ratio - c[1]))[0]


# ─────────────────────────────────────────────────────────
# VLM 模板（图片无 metadata 时，让多模态模型回填）
# ─────────────────────────────────────────────────────────
VLM_TEMPLATE = """请把这张图反向解析成可复现的 Text-to-Image 提示词，输出严格的 JSON：

{
  "subject": "图中主体的中文一句话描述（人/物/场景核心）",
  "subject_en": "subject in English",
  "style_preset": "从这 88 个预设里选一个最贴近的：写实摄影 / 胶片摄影 / 黑白摄影 / 人像摄影 / 时尚大片 / 美食摄影 / 产品摄影 / 微距摄影 / 航拍摄影 / 街拍纪实 / 暗黑美食 / 日杂 / 街头潮流 / 动漫 / 新海诚 / 宫崎骏 / 美漫 / Q版 / 童话绘本 / 萌系 / 厚涂 / 轻小说封面 / 赛璐璐 / 水彩 / 油画 / 水墨 / 工笔国画 / 浮世绘 / 线稿 / 像素艺术 / 3DC4D / 盲盒手办 / 低多边形 / 等距视图 / 粘土 / 毛毡手工 / 纸艺 / 极简主义 / 平面设计 / Logo设计 / 图标设计 / 信息图 / 品牌KV / 专辑封面 / 复古海报 / 电影海报 / 表情包 / 玻璃拟态 / 新拟态 / 孟菲斯 / 杂志编排 / 包豪斯 / 奶油风 / 印象派 / 后印象派 / 新艺术 / 装饰艺术 / 赛博朋克 / 蒸汽朋克 / 科幻 / 奇幻 / 黑暗奇幻 / 国潮 / Y2K / Vaporwave / 霓虹灯牌 / 建筑可视化 / 电影感 / 概念艺术 / 粗野主义 / 北欧极简 / 侘寂 / 疗愈治愈 / 美式复古 / 原神 / 崩铁星穹 / 英雄联盟 / 暗黑4 / Valorant / Pokemon / 暴雪风 / 敦煌壁画 / 青花瓷 / 民国月份牌 / 年画 / 剪纸 / 和风 / 汉服写真",
  "aspect": "1:1 / 3:4 / 16:9 / 21:9 / 9:16",
  "camera": "镜头/视角/焦段，例：'85mm telephoto, low angle, shallow depth of field'",
  "lighting": "光影描述，例：'golden hour rim light, soft fill'",
  "palette": "主色板，例：'muted earth tones, sage green and terracotta'",
  "composition": "构图特征：特写/近景/中景/全身/俯拍/仰拍/航拍/侧面/背面",
  "mood": "情绪：温暖/冷峻/神秘/梦幻/欢快/史诗/治愈/紧张",
  "time_of_day": "清晨/黄昏/日落/深夜/蓝调时刻 等（无则填空）",
  "weather": "晴/雨/雾/雪 等（无则填空）",
  "season": "春/夏/秋/冬/樱花季/枫叶季（无则填空）",
  "key_details": ["关键视觉元素 1", "元素 2", "元素 3"],
  "negatives": ["应避免出现的事物（用于负面提示）"],
  "suggested_prompt": "完整可直接喂给 Midjourney 的英文提示词（不含 --ar 参数）"
}

只输出 JSON，不要解释。
"""


# ─────────────────────────────────────────────────────────
# IO
# ─────────────────────────────────────────────────────────
def load_image_bytes(src: str) -> bytes:
    if src.startswith(("http://", "https://")):
        req = Request(src, headers={"User-Agent": "huo15-reverse/1.0"})
        with urlopen(req, timeout=15) as r:
            return r.read()
    with open(os.path.expanduser(src), "rb") as f:
        return f.read()


# ─────────────────────────────────────────────────────────
# 主反解流程
# ─────────────────────────────────────────────────────────
def reverse(src: str, vlm: bool = False) -> Dict:
    blob = load_image_bytes(src)
    is_png = blob.startswith(b"\x89PNG\r\n\x1a\n")
    meta = read_png_text_chunks(blob) if is_png else {}
    source = detect_source(meta)
    parsed: Dict[str, str] = {}

    if source == "a1111":
        parsed = parse_a1111_params(meta.get("parameters", ""))
    elif source == "comfyui":
        parsed = {"comfy_workflow": meta.get("workflow", "")[:200] + "...", "raw_prompt_json": meta.get("prompt", "")[:500]}
        try:
            data = json.loads(meta.get("prompt", "{}"))
            for node_id, node in data.items():
                if isinstance(node, dict) and node.get("class_type") in ("CLIPTextEncode", "CLIPTextEncodeSDXL"):
                    txt = (node.get("inputs") or {}).get("text", "")
                    if txt:
                        if "positive" not in parsed:
                            parsed["positive"] = txt
                        elif "negative" not in parsed:
                            parsed["negative"] = txt
        except (json.JSONDecodeError, AttributeError):
            pass
    elif source == "novelai":
        parsed = {
            "positive": meta.get("Description", ""),
            "comment": meta.get("Comment", "")[:500],
        }

    positive = parsed.get("positive", "")
    suggested_preset = guess_preset(positive) if positive else ""
    suggested_aspect = guess_aspect(parsed.get("size", ""))

    out: Dict = {
        "version": VERSION,
        "source": source,
        "file_size_bytes": len(blob),
        "is_png": is_png,
        "raw_metadata_keys": list(meta.keys()),
        "parsed": parsed,
        "suggested": {
            "preset": suggested_preset,
            "aspect": suggested_aspect,
            "seed": parsed.get("seed", ""),
            "model": parsed.get("model", ""),
            "sampler": parsed.get("sampler", ""),
            "cfg": parsed.get("cfg_scale", ""),
            "steps": parsed.get("steps", ""),
        },
    }

    if vlm or source in ("unknown", ""):
        out["vlm_template"] = VLM_TEMPLATE
        out["vlm_instructions"] = (
            "图中没有可读 metadata 或 metadata 不完整。请把图 + 上面 vlm_template 一起发给"
            " GPT-4o / Claude Sonnet 4.6 / Gemini 1.5 Pro / Qwen-VL，得到结构化 JSON 后，"
            "用 enhance_prompt.py \"<subject>\" -p \"<style_preset>\" -a \"<aspect>\" 复现。"
        )

    return out


def to_mj_prompt(result: Dict) -> str:
    p = result.get("parsed", {})
    pos = p.get("positive", "")
    aspect = result.get("suggested", {}).get("aspect", "")
    seed = result.get("suggested", {}).get("seed", "")
    flags = []
    if aspect:
        flags.append(f"--ar {aspect}")
    if seed:
        flags.append(f"--seed {seed}")
    flags.append("--stylize 250")
    return f"{pos} {' '.join(flags)}".strip()


def print_result(r: Dict):
    sep = "═" * 60
    print(f"\n{sep}")
    print(f"🔍 参考图反解 v{r['version']}")
    print(f"📁 文件大小   : {r['file_size_bytes']:,} bytes")
    print(f"🏷  来源识别   : {r['source']}")
    print(f"🗂  metadata 字段: {', '.join(r['raw_metadata_keys']) or '(无)'}")

    p = r.get("parsed", {})
    if p.get("positive"):
        print(f"\n✅ 反解正向提示：\n{p['positive']}")
    if p.get("negative"):
        print(f"\n❌ 反解负向提示：\n{p['negative']}")

    s = r.get("suggested", {})
    if any(s.values()):
        print(f"\n💡 推荐参数：")
        for k, v in s.items():
            if v:
                print(f"   {k:8s}: {v}")

    if r.get("vlm_template"):
        print(f"\n🤖 VLM 模板（图无 metadata 时使用）：")
        print(r.get("vlm_instructions", ""))
        print("\n--- 模板开始 ---")
        print(r["vlm_template"])
        print("--- 模板结束 ---")

    print(f"{sep}\n")


def main():
    parser = argparse.ArgumentParser(
        description=f"huo15-img-test reverse_prompt v{VERSION} — 参考图反解",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
示例:
  reverse_prompt.py /path/to/image.png             # 自动识别 A1111/ComfyUI/NovelAI metadata
  reverse_prompt.py https://example.com/img.png    # 远程 URL
  reverse_prompt.py img.png --vlm                  # 强制输出 VLM 模板（图无 metadata）
  reverse_prompt.py img.png --mj                   # 直接给出 Midjourney 复用 prompt
  reverse_prompt.py img.png -j                     # JSON 输出，可 pipe 给 enhance_prompt.py
""",
    )
    parser.add_argument("source", help="图片本地路径或 URL")
    parser.add_argument("--vlm", action="store_true", help="无论 metadata 是否齐全，都输出 VLM 模板")
    parser.add_argument("--mj", action="store_true", help="只输出 Midjourney 风格 prompt 一行")
    parser.add_argument("-j", "--json", action="store_true", help="JSON 输出")
    parser.add_argument("-v", "--version", action="version", version=f"%(prog)s v{VERSION}")
    args = parser.parse_args()

    try:
        r = reverse(args.source, vlm=args.vlm)
    except FileNotFoundError:
        print(f"❌ 找不到文件: {args.source}", file=sys.stderr)
        sys.exit(1)
    except Exception as e:
        print(f"❌ 加载失败: {e}", file=sys.stderr)
        sys.exit(1)

    if args.mj:
        print(to_mj_prompt(r))
        return
    if args.json:
        print(json.dumps(r, ensure_ascii=False, indent=2))
        return
    print_result(r)


if __name__ == "__main__":
    main()


path: scripts/safety_lint.py
content: #!/usr/bin/env python3
"""
huo15-img-test — 平台合规润色 v2.3

把"会被 SD/MJ/DALL-E 误判但本意是合法艺术创作"的描述，做艺术化重写，
提高过审率。**不是 jailbreak**，不做也拒绝以下场景：

  ✗ CSAM（任何含未成年 + 性化描述的）
  ✗ 真人 + 色情/裸露
  ✗ 真人 + 政治污蔑/诽谤
  ✗ 制作武器、毒品、爆炸物的具体方法
  ✗ 自残、自杀的诱导画面
  ✗ 仇恨/种族歧视画面

  ✓ 历史/神话/魔幻战斗（"血" → crimson splash）
  ✓ 古典艺术裸体（"裸" → classical nude figure study, art reference）
  ✓ 黑暗童话/惊悚氛围（"恐怖" → eerie atmospheric tension）
  ✓ 赛博朋克义体/机甲伤痕（"伤" → battle-scarred armor texture）
  ✓ 医学/解剖艺术插画

工作流程：
  1. 红线检测：扫描禁区词组合，命中即拒
  2. 黄区识别：标识可能误判的词
  3. 艺术替代：内置 80+ 艺术化重写词典
  4. 平台分级：DALL-E 最严 / MJ 中等 / SD 宽松（开源），分别给出建议

调用：
  safety_lint.py "战士手中沾满鲜血的剑"
  safety_lint.py "古典维纳斯雕像" --target dalle
  safety_lint.py "赛博朋克战斗场景" -j
"""

import sys
import os
import json
import re
import argparse
from typing import Dict, List, Tuple

VERSION = "2.3.0"

# ─────────────────────────────────────────────────────────
# 红线（直接拒答）
# ─────────────────────────────────────────────────────────
HARD_BLOCKS: List[Tuple[str, str]] = [
    # 未成年 + 性化（任意组合都拒）
    (r"(loli|lolita|underage|child|minor|kid|toddler|baby|teen|teenager|13岁|14岁|15岁|16岁|17岁|未成年|幼女|小学生|初中生|学生)\s*.*(nude|naked|sex|nsfw|porn|裸|性|色情|nipple|breast)", "minor + sexual"),
    (r"(nude|naked|sex|nsfw|porn|裸|性|色情)\s*.*(loli|underage|child|minor|kid|toddler|baby|teen|未成年|幼女|小学生|初中生)", "minor + sexual"),
    # 真人 + 色情（指名道姓）
    (r"(taylor swift|emma watson|scarlett johansson|某某明星|周杰伦|王力宏)\s*.*(nude|naked|sex|porn|裸|色情)", "real-person + sexual"),
    # 武器制造、毒品、爆炸物配方（双向：动词在前 OR 在后）
    (r"(how to make|recipe for|tutorial|step.*by.*step|步骤|配方|怎么做|如何制作|教程)\s*.*(bomb|explosive|gun|firearm|meth|cocaine|heroin|fentanyl|nitroglycerin|炸弹|手枪|冰毒|海洛因|芬太尼|硝酸|tnt)", "weapon/drug instruction"),
    (r"(bomb|explosive|gun|firearm|meth|cocaine|heroin|fentanyl|炸弹|手枪|冰毒|海洛因|芬太尼)\s*.*(how to make|recipe|tutorial|步骤|配方|怎么做|如何制作|教程|方法)", "weapon/drug instruction"),
    # 自残诱导（双向）
    (r"(suicide|self-harm|cutting|自杀|自残|割腕|跳楼)\s*.*(method|how to|tutorial|教程|方法|步骤)", "self-harm method"),
    (r"(method|how to|tutorial|教程|方法|步骤)\s*.*(suicide|self-harm|cutting|自杀|自残|割腕|跳楼)", "self-harm method"),
]

# ─────────────────────────────────────────────────────────
# 黄区：会被误判但通常合法的艺术词 → 艺术化替代
# ─────────────────────────────────────────────────────────
ART_SUBSTITUTIONS: Dict[str, Dict[str, str]] = {
    # 战斗 / 暴力（合法艺术语境）
    "blood": {"replace": "crimson splash, dramatic battle highlight", "category": "violence", "platforms": "DALL-E,MJ"},
    "鲜血": {"replace": "crimson splash, 朱砂色泼洒", "category": "violence", "platforms": "DALL-E,MJ"},
    "血": {"replace": "crimson splash", "category": "violence", "platforms": "DALL-E"},
    "wound": {"replace": "battle-scarred texture", "category": "violence", "platforms": "DALL-E"},
    "伤口": {"replace": "battle-scarred texture, 战痕", "category": "violence", "platforms": "DALL-E"},
    "kill": {"replace": "defeat, vanquish", "category": "violence", "platforms": "DALL-E,MJ"},
    "杀": {"replace": "vanquish, 击败", "category": "violence", "platforms": "DALL-E,MJ"},
    "murder": {"replace": "dramatic confrontation", "category": "violence", "platforms": "DALL-E,MJ"},
    "weapon": {"replace": "ceremonial blade, ornamental armament", "category": "violence", "platforms": "DALL-E"},
    "gun": {"replace": "fantasy ranged weapon, prop firearm", "category": "violence", "platforms": "DALL-E"},
    "knife": {"replace": "ornamental dagger, ritual blade", "category": "violence", "platforms": "DALL-E"},
    "violence": {"replace": "dynamic combat, cinematic action", "category": "violence", "platforms": "DALL-E,MJ"},
    "暴力": {"replace": "dynamic combat scene, 动作张力", "category": "violence", "platforms": "DALL-E,MJ"},

    # 古典艺术裸体
    "naked": {"replace": "classical nude figure study, art reference, marble sculpture style", "category": "nudity", "platforms": "DALL-E,MJ,SD"},
    "nude": {"replace": "classical figure study, fine art reference", "category": "nudity", "platforms": "DALL-E,MJ"},
    "裸": {"replace": "classical figure study, 古典裸体艺术", "category": "nudity", "platforms": "DALL-E,MJ,SD"},
    "裸体": {"replace": "classical figure study, 古典维纳斯", "category": "nudity", "platforms": "DALL-E,MJ,SD"},
    "sexy": {"replace": "elegant alluring, fashion editorial", "category": "nudity", "platforms": "DALL-E"},
    "性感": {"replace": "elegant fashion editorial, 优雅造型", "category": "nudity", "platforms": "DALL-E"},
    "lingerie": {"replace": "vintage fashion sleepwear, 1950s glamour", "category": "nudity", "platforms": "DALL-E"},
    "bikini": {"replace": "summer beachwear, swimwear photography", "category": "nudity", "platforms": "DALL-E"},

    # 恐怖 / 黑暗
    "horror": {"replace": "eerie atmospheric tension, gothic mood", "category": "horror", "platforms": "DALL-E"},
    "恐怖": {"replace": "gothic atmospheric tension, 哥特氛围", "category": "horror", "platforms": "DALL-E"},
    "scary": {"replace": "ominous mood, atmospheric suspense", "category": "horror", "platforms": "DALL-E"},
    "gore": {"replace": "dark fantasy aesthetic, baroque dramatic", "category": "horror", "platforms": "DALL-E,MJ"},
    "monster": {"replace": "mythical creature, fantasy beast", "category": "horror", "platforms": "DALL-E"},
    "demon": {"replace": "mythical entity, dark fantasy spirit", "category": "horror", "platforms": "DALL-E"},
    "evil": {"replace": "dark mythological aesthetic, 黑暗神话", "category": "horror", "platforms": "DALL-E"},

    # 死亡 / 尸体（艺术语境）
    "dead": {"replace": "fallen, resting eternal", "category": "death", "platforms": "DALL-E"},
    "death": {"replace": "memento mori, classical allegory", "category": "death", "platforms": "DALL-E"},
    "corpse": {"replace": "still figure, classical allegorical pose", "category": "death", "platforms": "DALL-E"},
    "skeleton": {"replace": "anatomical skeletal study, da vinci sketch reference", "category": "death", "platforms": "DALL-E"},
    "skull": {"replace": "memento mori symbol, vanitas still life", "category": "death", "platforms": "DALL-E"},

    # 真人
    "celebrity": {"replace": "fictional character inspired by 80s aesthetic", "category": "real-person", "platforms": "DALL-E,MJ"},
    "明星": {"replace": "虚构角色，80年代美学风格", "category": "real-person", "platforms": "DALL-E,MJ"},
    "actor": {"replace": "fictional protagonist, original character", "category": "real-person", "platforms": "DALL-E"},
    "politician": {"replace": "fictional statesman character", "category": "real-person", "platforms": "DALL-E,MJ"},

    # 品牌（版权）
    "marvel": {"replace": "superhero comic style", "category": "brand", "platforms": "DALL-E"},
    "disney": {"replace": "classic animated film style", "category": "brand", "platforms": "DALL-E"},
    "nike": {"replace": "athletic sportswear brand aesthetic", "category": "brand", "platforms": "DALL-E"},
    "iphone": {"replace": "modern smartphone, sleek minimal device", "category": "brand", "platforms": "DALL-E"},

    # 武器具体型号
    "ak47": {"replace": "fictional assault rifle prop", "category": "weapon-model", "platforms": "DALL-E,MJ"},
    "ak-47": {"replace": "fictional assault rifle prop", "category": "weapon-model", "platforms": "DALL-E,MJ"},
    "glock": {"replace": "sci-fi handgun prop", "category": "weapon-model", "platforms": "DALL-E,MJ"},
    "uzi": {"replace": "compact fictional firearm prop", "category": "weapon-model", "platforms": "DALL-E,MJ"},
}

# 平台分级规则
PLATFORM_STRICTNESS = {
    "dalle": "max",      # 最严
    "DALL-E": "max",
    "midjourney": "high", # 中等
    "MJ": "high",
    "mj": "high",
    "sd": "low",         # 宽松（本地）
    "SD": "low",
    "sdxl": "low",
    "flux": "low",
    "comfyui": "low",
}

# 风险等级 → 颜色 emoji
RISK_LEVEL_EMOJI = {"high": "🔴", "medium": "🟡", "low": "🟢"}


def category_risk_for_platform(category: str, platform: str) -> str:
    s = PLATFORM_STRICTNESS.get(platform, "high")
    risk_map = {
        "violence":     {"max": "high", "high": "medium", "low": "low"},
        "nudity":       {"max": "high", "high": "high",   "low": "medium"},
        "horror":       {"max": "medium", "high": "low",  "low": "low"},
        "death":        {"max": "medium", "high": "low",  "low": "low"},
        "real-person":  {"max": "high", "high": "high",   "low": "medium"},
        "brand":        {"max": "high", "high": "medium", "low": "low"},
        "weapon-model": {"max": "high", "high": "high",   "low": "medium"},
    }
    return risk_map.get(category, {}).get(s, "low")


# ─────────────────────────────────────────────────────────
# 检测
# ─────────────────────────────────────────────────────────
def check_hard_blocks(text: str) -> List[str]:
    """返回命中的红线类别（命中任何一个即拒答）。"""
    hits = []
    lower = text.lower()
    for pattern, label in HARD_BLOCKS:
        if re.search(pattern, lower, re.IGNORECASE):
            hits.append(label)
    return hits


def find_substitutions(text: str, platform: str = "MJ") -> List[Dict]:
    """识别文本中的黄区词，返回替代建议列表。"""
    out = []
    lower = text.lower()
    seen = set()
    for word, info in ART_SUBSTITUTIONS.items():
        if word in seen:
            continue
        # 中文词直接子串匹配，英文词加单词边界
        if re.fullmatch(r"[\x00-\x7f]+", word):  # ASCII
            if not re.search(r"\b" + re.escape(word.lower()) + r"\b", lower):
                continue
        else:
            if word not in text:
                continue
        seen.add(word)
        risk = category_risk_for_platform(info["category"], platform)
        out.append({
            "word": word,
            "replace_with": info["replace"],
            "category": info["category"],
            "risk_for_platform": risk,
            "platforms_affected": info["platforms"],
        })
    return out


def rewrite(text: str, platform: str = "MJ") -> Tuple[str, List[Dict]]:
    """执行重写：把所有黄区词替换成艺术化版本。返回 (新文本, 替换日志)。"""
    new_text = text
    log = []
    for word, info in ART_SUBSTITUTIONS.items():
        if re.fullmatch(r"[\x00-\x7f]+", word):
            pat = re.compile(r"\b" + re.escape(word) + r"\b", re.IGNORECASE)
        else:
            pat = re.compile(re.escape(word))
        if pat.search(new_text):
            risk = category_risk_for_platform(info["category"], platform)
            new_text = pat.sub(info["replace"], new_text)
            log.append({"from": word, "to": info["replace"], "category": info["category"],
                        "risk_for_platform": risk})
    return new_text, log


# ─────────────────────────────────────────────────────────
# 入口
# ─────────────────────────────────────────────────────────
def lint(text: str, platform: str = "MJ") -> Dict:
    blocks = check_hard_blocks(text)
    if blocks:
        return {
            "version": VERSION,
            "platform": platform,
            "verdict": "REJECT",
            "reason": "hit hard-block patterns",
            "categories": blocks,
            "advice": (
                "命中红线规则。本工具不服务以下场景：\n"
                "  • CSAM（任何含未成年 + 性化）\n"
                "  • 真人 + 色情 / 政治污蔑\n"
                "  • 武器/毒品/爆炸物制作教程\n"
                "  • 自残/自杀方法诱导\n"
                "如果你的本意是合法艺术创作（历史/神话/古典），请改写描述：\n"
                "  • 用成年角色\n"
                "  • 用艺术语境（古典雕塑/神话/壁画）\n"
                "  • 不要点名真人\n"
                "  • 不要含「教程/步骤/方法」等指令性词"
            ),
        }

    subs = find_substitutions(text, platform)
    rewritten, log = rewrite(text, platform)

    return {
        "version": VERSION,
        "platform": platform,
        "verdict": "OK" if not subs else "REWRITE",
        "original": text,
        "rewritten": rewritten,
        "substitutions": subs,
        "rewrite_log": log,
        "high_risk_count": sum(1 for s in subs if s["risk_for_platform"] == "high"),
        "medium_risk_count": sum(1 for s in subs if s["risk_for_platform"] == "medium"),
        "advice": _build_advice(platform, subs),
    }


def _build_advice(platform: str, subs: List[Dict]) -> str:
    if not subs:
        return f"无风险词，可直接喂给 {platform}。"
    lines = [f"针对 {platform}（严格度: {PLATFORM_STRICTNESS.get(platform, 'high')}）的合规建议："]
    high = [s for s in subs if s["risk_for_platform"] == "high"]
    if high:
        lines.append(f"  🔴 {len(high)} 个高风险词建议必换：" + ", ".join(s["word"] for s in high))
    med = [s for s in subs if s["risk_for_platform"] == "medium"]
    if med:
        lines.append(f"  🟡 {len(med)} 个中风险词建议软化：" + ", ".join(s["word"] for s in med))
    return "\n".join(lines)


def print_lint(r: Dict):
    sep = "═" * 60
    print(f"\n{sep}")
    print(f"🛡  平台合规润色 v{r['version']}")
    print(f"📺 目标平台: {r['platform']} (严格度: {PLATFORM_STRICTNESS.get(r['platform'], '?')})")

    if r["verdict"] == "REJECT":
        print(f"\n🚫 拒答: {r['reason']}")
        print(f"   命中类别: {', '.join(r['categories'])}")
        print(f"\n{r['advice']}")
        print(f"{sep}\n")
        return

    print(f"📝 原文: {r['original']}")
    if r["verdict"] == "OK":
        print(f"\n✅ 无风险词，原文可直接使用")
        print(f"{sep}\n")
        return

    print(f"\n✨ 重写后: {r['rewritten']}")
    print(f"\n📊 风险统计: 🔴 {r['high_risk_count']} / 🟡 {r['medium_risk_count']}")

    if r["substitutions"]:
        print(f"\n🔄 替换详情:")
        for s in r["substitutions"]:
            emoji = RISK_LEVEL_EMOJI.get(s["risk_for_platform"], "⚪")
            print(f"   {emoji} '{s['word']}' → '{s['replace_with']}'")
            print(f"      类别: {s['category']}, 平台: {s['platforms_affected']}")

    print(f"\n💡 {r['advice']}")
    print(f"{sep}\n")


def main():
    parser = argparse.ArgumentParser(
        description=f"huo15-img-test safety_lint v{VERSION} — 平台合规润色（合法艺术创作专用）",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
示例:
  safety_lint.py "战士手中沾满鲜血的剑"
  safety_lint.py "古典维纳斯雕像" --target dalle
  safety_lint.py "赛博朋克战斗场景" --target SD
  safety_lint.py "黑暗骑士" -j
  echo "原始描述" | safety_lint.py --stdin --apply  # 重写并输出新文本

注意: 本工具仅服务合法艺术创作。拒绝以下场景：
  ✗ CSAM（未成年 + 性化）
  ✗ 真人 + 色情/诽谤
  ✗ 武器/毒品/爆炸物制作教程
  ✗ 自残诱导
""",
    )
    parser.add_argument("text", nargs="?", help="要检查的文本")
    parser.add_argument("--stdin", action="store_true", help="从 stdin 读取")
    parser.add_argument("--target", default="MJ",
                        help="目标平台 DALL-E/MJ/SD/SDXL/Flux/通用 (默认 MJ)")
    parser.add_argument("--apply", action="store_true",
                        help="直接输出重写后的文本（用于 pipe）")
    parser.add_argument("-j", "--json", action="store_true", help="JSON 输出")
    parser.add_argument("-v", "--version", action="version", version=f"%(prog)s v{VERSION}")
    args = parser.parse_args()

    text = args.text
    if args.stdin or not text:
        if sys.stdin.isatty() and not args.stdin:
            parser.print_help()
            sys.exit(1)
        text = sys.stdin.read().strip()

    if not text:
        print("❌ 输入为空", file=sys.stderr)
        sys.exit(1)

    r = lint(text, args.target)

    if args.apply:
        if r["verdict"] == "REJECT":
            print(f"REJECTED: {r['reason']}", file=sys.stderr)
            sys.exit(2)
        print(r.get("rewritten", r["original"]))
        return

    if args.json:
        print(json.dumps(r, ensure_ascii=False, indent=2))
        return
    print_lint(r)
    if r["verdict"] == "REJECT":
        sys.exit(2)


if __name__ == "__main__":
    main()


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