优化SKILL
parent
286e28b7f3
commit
68c9ed7bcb
|
|
@ -25,10 +25,14 @@ maomomo-article-writer/
|
|||
├── README.md # 面向使用者的说明文档
|
||||
├── agents/
|
||||
│ └── openai.yaml # 技能展示名称、简介和默认提示语
|
||||
├── scripts/
|
||||
│ └── maomomo_image_gen.py # OpenAI 兼容 API/CLI 配图 fallback
|
||||
└── references/
|
||||
├── source-and-fact-check.md # 来源读取、事实核查和风险口径
|
||||
├── style-guide.md # MAOMOMO 写作风格、模板和文章骨架
|
||||
├── visual-assets.md # 配图清单、视觉规则和图片 QA
|
||||
├── recommended-styles.md # 推荐配图风格和提示词片段
|
||||
├── image-generation.md # 图片生成脚本、manifest 和后端选择
|
||||
└── delivery-rules.md # 完整交付、打包和最终检查
|
||||
```
|
||||
|
||||
|
|
@ -122,7 +126,7 @@ maomomo-article-writer
|
|||
2. 读取来源并列出已读 / 未读材料,整理事实表和待确认项。
|
||||
3. 确认文章范围、大纲、标题方向、核心口径和图片规划。
|
||||
4. 写作或优化发布用 Markdown。
|
||||
5. 生成或准备独立 PNG 配图,插入 Markdown。
|
||||
5. 确认配图风格,生成或准备独立 PNG 配图,插入 Markdown。
|
||||
6. 检查来源、图片路径、风险提示、编辑痕迹和 ZIP 内容,最后交付文件路径。
|
||||
|
||||
用户明确说“只要文字”“只要大纲”“不要图片”“不要打包”时,会按用户限制执行,不强行创建完整发布包。
|
||||
|
|
@ -147,6 +151,7 @@ maomomo-article-writer
|
|||
- 语气像懂行朋友,不像官方宣传稿
|
||||
- 涉及金融、券商、银行、数字货币和合规内容时,保留谨慎措辞
|
||||
- 配图统一使用白橘猫、暖橙色、干净金融科技教程风
|
||||
- 如果用户没有指定配图风格,先在 `warm-fintech-guide`、`data-card-dashboard`、`clean-editorial`、`handdrawn-note`、`xiaohongshu-vertical` 中推荐 2-3 个方向。
|
||||
|
||||
文章不应当:
|
||||
|
||||
|
|
@ -161,6 +166,8 @@ maomomo-article-writer
|
|||
- 详细文章模板、标题套路和常用词写在 `references/style-guide.md`。
|
||||
- 来源核查、时效性、冲突口径和风险表达写在 `references/source-and-fact-check.md`。
|
||||
- 配图类型、猫咪视觉、文件命名和图片 QA 写在 `references/visual-assets.md`。
|
||||
- 推荐配图风格、适用场景和提示词片段写在 `references/recommended-styles.md`。
|
||||
- 图片 API/CLI fallback、批量 manifest 和脚本用法写在 `references/image-generation.md`,脚本为 `scripts/maomomo_image_gen.py`。
|
||||
- 完整交付、ZIP、附件和最终检查写在 `references/delivery-rules.md`。
|
||||
- 修改技能展示名称或默认提示语时,同步更新 `agents/openai.yaml`。
|
||||
- 如果新增具体行业资料、平台规则或长期复用模板,优先放入 `references/`,避免让 `SKILL.md` 过长。
|
||||
|
|
|
|||
6
SKILL.md
6
SKILL.md
|
|
@ -70,8 +70,10 @@ description: 生成或优化符合 maomomo / 猫MOMO 风格的中文出海金融
|
|||
|
||||
5. 配图规划和生成。
|
||||
- 需要配图时读 `references/visual-assets.md`。
|
||||
- 先生成图片清单:类型、标题、放置位置、核心文案、文件名、alt text、是否依赖用户截图。
|
||||
- 若用户没有指定视觉方向,读 `references/recommended-styles.md`,先提供 2-3 个 MAOMOMO 配图风格选项并推荐一个;确认后整篇文章保持同一视觉身份。
|
||||
- 先生成图片清单:类型、标题、放置位置、核心文案、文件名、alt text、风格、是否依赖用户截图。
|
||||
- 每张图独立生成或准备为高清 PNG,放入 `assets/`,用相对路径插入 Markdown。
|
||||
- 优先使用当前 agent 的内置图片生成工具;当用户要求 API/CLI、内置工具不可用或需要批量 manifest 时,读 `references/image-generation.md` 并使用 `scripts/maomomo_image_gen.py`。
|
||||
- 没有真实截图时只能生成“示意图 / 信息图”,不能伪造官方 App 截图。
|
||||
|
||||
6. 打包与最终 QA。
|
||||
|
|
@ -117,4 +119,6 @@ description: 生成或优化符合 maomomo / 猫MOMO 风格的中文出海金融
|
|||
- `references/source-and-fact-check.md`:来源读取、事实表、时效性、冲突口径和金融风险核查。
|
||||
- `references/style-guide.md`:MAOMOMO 文章类型、标题套路、语气、结构模板、小红书模式和写作细节。
|
||||
- `references/visual-assets.md`:配图类型、猫咪视觉、文件命名、Markdown 插图、禁止伪造截图和图片 QA。
|
||||
- `references/recommended-styles.md`:推荐配图风格、适用场景、推荐话术和提示词片段。
|
||||
- `references/image-generation.md`:图片生成后端选择、`scripts/maomomo_image_gen.py` 用法、manifest 和故障处理。
|
||||
- `references/delivery-rules.md`:完整交付、ZIP、更新打包、附件策略和最终检查清单。
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
interface:
|
||||
display_name: "MAOMOMO 文章生成器"
|
||||
short_description: "阶段化生成 MAOMOMO 文章、配图和 ZIP 发布包"
|
||||
default_prompt: "使用 $maomomo-article-writer 生成或优化一篇 MAOMOMO 风格的中文出海金融实操文章。请先核查来源和口径,再交付 Markdown、assets 独立配图目录和 ZIP 包。"
|
||||
short_description: "阶段化生成 MAOMOMO 文章、多风格配图和 ZIP 发布包"
|
||||
default_prompt: "使用 $maomomo-article-writer 生成或优化一篇 MAOMOMO 风格的中文出海金融实操文章。请先核查来源和口径,再确认配图风格,最后交付 Markdown、assets 独立配图目录和 ZIP 包。"
|
||||
|
||||
policy:
|
||||
allow_implicit_invocation: true
|
||||
|
|
|
|||
|
|
@ -0,0 +1,107 @@
|
|||
# 图片生成脚本与后端选择
|
||||
|
||||
需要实际生成配图文件、使用 API/CLI fallback、批量生成 assets,或排查图片接口配置时,先读本文件。
|
||||
|
||||
## 后端选择
|
||||
|
||||
优先顺序:
|
||||
|
||||
1. Codex / 当前 agent 的内置图片生成工具。适合大多数配图任务。
|
||||
2. `scripts/maomomo_image_gen.py` API/CLI fallback。适合用户明确要求 API/CLI、内置工具不可用、需要批量 manifest、或希望把生成过程记录到文件时。
|
||||
|
||||
不要因为脚本存在就强制使用脚本。若内置图片工具可用,仍优先使用内置工具生成独立 PNG,再保存到 `assets/`。
|
||||
|
||||
## 配置来源
|
||||
|
||||
脚本按以下顺序读取配置:
|
||||
|
||||
- `MAOMOMO_IMAGE_API_KEY` / `OPENAI_API_KEY`
|
||||
- `MAOMOMO_IMAGE_BASE_URL` / `OPENAI_BASE_URL`
|
||||
- `MAOMOMO_IMAGE_MODEL` / `CODEX_PPT_IMAGE_MODEL`
|
||||
- `~/.codex/config.toml` 中的 `base_url`
|
||||
- `~/.codex/auth.json` 中可识别的 API key
|
||||
|
||||
不要在日志、回答、Markdown 或 manifest 中写入完整 API key。
|
||||
|
||||
## 单张生成
|
||||
|
||||
```bash
|
||||
python3 scripts/maomomo_image_gen.py generate \
|
||||
--out article/assets/hsbc-mastercard-alipay-campaign-01-cover.png \
|
||||
--style warm-fintech-guide \
|
||||
--title "汇丰 Mastercard 支付宝活动" \
|
||||
--image-type "封面图" \
|
||||
--core-text "先看报名、门槛和封顶,避免白刷"
|
||||
```
|
||||
|
||||
先只看 prompt:
|
||||
|
||||
```bash
|
||||
python3 scripts/maomomo_image_gen.py generate \
|
||||
--out article/assets/hsbc-mastercard-alipay-campaign-01-cover.png \
|
||||
--style data-card-dashboard \
|
||||
--title "返现计算图" \
|
||||
--image-type "计算图" \
|
||||
--core-text "HK$190 ÷ 9% ≈ HK$2112,超过部分没有额外收益" \
|
||||
--dry-run
|
||||
```
|
||||
|
||||
## 批量 manifest
|
||||
|
||||
manifest 示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"images": [
|
||||
{
|
||||
"file_name": "assets/hsbc-mastercard-alipay-campaign-01-cover.png",
|
||||
"type": "封面图",
|
||||
"title": "汇丰 Mastercard 支付宝活动",
|
||||
"core_text": "先报名,再看门槛和封顶",
|
||||
"alt_text": "MAOMOMO 汇丰 Mastercard 支付宝返现活动封面图",
|
||||
"style": "warm-fintech-guide",
|
||||
"aspect_ratio": "16:9"
|
||||
},
|
||||
{
|
||||
"file_name": "assets/hsbc-mastercard-alipay-campaign-02-calculation.png",
|
||||
"type": "计算图",
|
||||
"title": "怎么刷到刚刚好",
|
||||
"core_text": "HK$190 ÷ 9% ≈ HK$2112,超过部分没有额外收益",
|
||||
"style": "data-card-dashboard"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
运行:
|
||||
|
||||
```bash
|
||||
python3 scripts/maomomo_image_gen.py batch \
|
||||
--manifest article/image_manifest.json \
|
||||
--base-dir article
|
||||
```
|
||||
|
||||
`file_name` 必须与 Markdown 中的相对路径一致,且最终文件必须真实存在。
|
||||
|
||||
## 内置风格名
|
||||
|
||||
可用:
|
||||
|
||||
- `warm-fintech-guide`
|
||||
- `clean-editorial`
|
||||
- `data-card-dashboard`
|
||||
- `handdrawn-note`
|
||||
- `xiaohongshu-vertical`
|
||||
|
||||
查看完整描述:
|
||||
|
||||
```bash
|
||||
python3 scripts/maomomo_image_gen.py styles
|
||||
```
|
||||
|
||||
## 失败处理
|
||||
|
||||
- API key 缺失:提示用户配置环境变量或检查 Codex 配置,不要编造图片文件。
|
||||
- 接口失败:保留已生成图片,说明失败项,必要时用 `--dry-run` 交付 prompt 清单作为临时排查材料。
|
||||
- 图片文字乱码或金额错误:重新生成该单张图片;不要手工覆盖文字伪装成模型输出。
|
||||
- 图片不符合风格:先更新 manifest 的 `style` / `core_text` / `extra`,再重跑对应项。
|
||||
|
|
@ -0,0 +1,136 @@
|
|||
# MAOMOMO 推荐配图风格
|
||||
|
||||
需要选择、推荐、复用或调整配图风格时,读本文件。风格名要与 `scripts/maomomo_image_gen.py` 的 `--style` 保持一致。
|
||||
|
||||
## 推荐顺序
|
||||
|
||||
默认优先推荐 `warm-fintech-guide`。如果文章明显更偏数字、对比、避坑或小红书,再切换到更合适的风格。
|
||||
|
||||
| 风格名 | 优先使用场景 | 不适合 |
|
||||
|---|---|---|
|
||||
| `warm-fintech-guide` | 常规 MAOMOMO 文章、港卡、银行活动、返现攻略、开户教程 | 数字极密集的计算页、小红书竖版 |
|
||||
| `data-card-dashboard` | 返现计算、费用测算、活动门槛、产品对比、时间线 | 情绪化避坑故事、截图驱动步骤 |
|
||||
| `clean-editorial` | SEO 长文、总结盘点、品牌感封面、解释型文章 | 需要强步骤感或大量数字卡片的图 |
|
||||
| `handdrawn-note` | 避坑经验、保号教程、操作步骤、实测提醒 | 严肃金融报告感、密集表格 |
|
||||
| `xiaohongshu-vertical` | 小红书、4 图看完、手机端竖版内容 | 常规网站横版封面和正文配图 |
|
||||
|
||||
## warm-fintech-guide
|
||||
|
||||
推荐语:
|
||||
|
||||
```text
|
||||
我建议用 warm-fintech-guide,因为它最贴近 MAOMOMO 默认视觉:白底、暖橙、猫咪向导和金融教程卡片,适合大多数港卡、银行活动和返现攻略。
|
||||
```
|
||||
|
||||
视觉要点:
|
||||
|
||||
- 明亮白底,暖橙色强调。
|
||||
- 白橘猫作为向导、提示牌或角落品牌元素。
|
||||
- 信息卡片清晰,数字、日期和步骤不拥挤。
|
||||
- 适合 16:9 横版封面、总览图、流程图。
|
||||
|
||||
提示词片段:
|
||||
|
||||
```text
|
||||
MAOMOMO 暖橙金融教程风,明亮白底,暖橙强调色,干净卡片布局,友好的白橘猫作为向导,现代 App 教程质感,中文标题清楚,包含 MAOMOMO 标识。
|
||||
```
|
||||
|
||||
## data-card-dashboard
|
||||
|
||||
推荐语:
|
||||
|
||||
```text
|
||||
我建议用 data-card-dashboard,因为这篇重点是规则、门槛和金额,读者需要一眼看懂数字和限制。
|
||||
```
|
||||
|
||||
视觉要点:
|
||||
|
||||
- 指标卡、对比表、时间线、计算公式。
|
||||
- 数字层级强,适合“封顶”“门槛”“成本”“到账时间”。
|
||||
- 猫咪比例较小,只做提示和品牌识别。
|
||||
- 适合返现计算图、路线对比图、活动总览图。
|
||||
|
||||
提示词片段:
|
||||
|
||||
```text
|
||||
MAOMOMO 数据卡片仪表盘风,白底,暖橙和蓝灰辅助色,使用指标卡、表格、时间线和计算公式展示关键信息,中文数字清晰可读,猫咪作为小型提示元素。
|
||||
```
|
||||
|
||||
## clean-editorial
|
||||
|
||||
推荐语:
|
||||
|
||||
```text
|
||||
我建议用 clean-editorial,因为这篇更像站内解释型长文,需要封面干净、标题有传播感,但不要太像促销海报。
|
||||
```
|
||||
|
||||
视觉要点:
|
||||
|
||||
- 大标题、留白、轻杂志感。
|
||||
- 猫咪放在角落或标题区附近,不抢正文信息。
|
||||
- 更适合封面、文章总结图、观点图。
|
||||
- 画面克制,避免过度卡通。
|
||||
|
||||
提示词片段:
|
||||
|
||||
```text
|
||||
MAOMOMO 清爽编辑部风,白底大标题,轻杂志排版,暖橙点缀,留白充足,白橘猫作为角落品牌元素,适合中文金融经验文章封面。
|
||||
```
|
||||
|
||||
## handdrawn-note
|
||||
|
||||
推荐语:
|
||||
|
||||
```text
|
||||
我建议用 handdrawn-note,因为这篇是避坑 / 步骤教程,手绘箭头、便签和圈注更容易把翻车点讲明白。
|
||||
```
|
||||
|
||||
视觉要点:
|
||||
|
||||
- 手绘箭头、便签、圈注、风险提示贴纸。
|
||||
- 适合“不要这样做”“先做这一步”“最大坑点”。
|
||||
- 信息密度中等,避免塞太多长句。
|
||||
- 猫咪可以举提示牌或指向关键步骤。
|
||||
|
||||
提示词片段:
|
||||
|
||||
```text
|
||||
MAOMOMO 手绘便签风,白底纸感,暖橙便利贴,手绘箭头和圈注,白橘猫举提示牌,中文步骤清楚,适合避坑经验和保姆式教程。
|
||||
```
|
||||
|
||||
## xiaohongshu-vertical
|
||||
|
||||
推荐语:
|
||||
|
||||
```text
|
||||
我建议用 xiaohongshu-vertical,因为用户要发小红书,应该重新做 9:16 竖图,而不是复用站内横版图。
|
||||
```
|
||||
|
||||
视觉要点:
|
||||
|
||||
- 9:16 竖版,手机端可读。
|
||||
- 大字少字,标题钩子强。
|
||||
- 4 图系列统一:封面、核心拆解、操作流程、避坑总结。
|
||||
- 每张图都有 MAOMOMO 标识或猫咪水印。
|
||||
|
||||
提示词片段:
|
||||
|
||||
```text
|
||||
MAOMOMO 小红书竖版攻略风,9:16,手机端可读,大标题,少量关键数字,暖橙白底,白橘猫水印,适合 4 图看完的中文金融攻略。
|
||||
```
|
||||
|
||||
## 风格选择输出格式
|
||||
|
||||
向用户推荐时使用:
|
||||
|
||||
```text
|
||||
我建议用 A,因为【一句话说明与文章类型的匹配】。
|
||||
|
||||
A. warm-fintech-guide(推荐):【适合原因】
|
||||
B. data-card-dashboard:【适合原因】
|
||||
C. handdrawn-note:【适合原因】
|
||||
|
||||
你选哪个?也可以指定更偏简洁、数据感、手绘感或小红书感。
|
||||
```
|
||||
|
||||
用户选择后,把风格名写入图片清单的 `风格` 字段或 manifest 的 `style` 字段。
|
||||
|
|
@ -11,6 +11,40 @@
|
|||
- 用户提供真实截图时,优先用真实截图,并隐去姓名、账号、余额、地址、手机号、二维码、交易号等个人信息。
|
||||
- 每张图必须包含清晰可见的 `MAOMOMO` 标识。
|
||||
|
||||
## 风格选择
|
||||
|
||||
如果用户没有指定视觉方向,生成图片前先读 `references/recommended-styles.md`,给 2-3 个可选风格,并推荐一个。确认后,整篇文章的配图保持同一套视觉身份:稳定的配色、猫咪造型、标题区、图标语言和信息密度;不同图片可以根据用途变化构图。
|
||||
|
||||
默认推荐逻辑:
|
||||
|
||||
- 活动 / 返现 / 港卡攻略:优先 `warm-fintech-guide`。
|
||||
- 规则、门槛、费用、返现计算和产品对比:优先 `data-card-dashboard`。
|
||||
- SEO 长文、经验整理、品牌感更强的文章:优先 `clean-editorial`。
|
||||
- 保姆式教程、避坑经验和轻量操作步骤:优先 `handdrawn-note`。
|
||||
- 小红书 / 4 图看完:必须用 `xiaohongshu-vertical`,并生成 9:16 竖图。
|
||||
|
||||
可选风格:
|
||||
|
||||
| 风格名 | 适合场景 | 视觉方向 |
|
||||
|---|---|---|
|
||||
| `warm-fintech-guide` | 默认 MAOMOMO 金融攻略、港卡、银行活动 | 白底、暖橙强调色、干净卡片、白橘猫向导 |
|
||||
| `clean-editorial` | SEO 长文、解释型文章、站内封面 | 大标题、留白、轻杂志感、猫咪作为角落品牌元素 |
|
||||
| `data-card-dashboard` | 返现计算、活动对比、费用路径、时间线 | 指标卡、表格感、仪表盘、清晰数字层级 |
|
||||
| `handdrawn-note` | 避坑、步骤教程、保号经验 | 手绘箭头、便签、圈注、猫咪提示牌 |
|
||||
| `xiaohongshu-vertical` | 小红书发布版、4 图看完 | 9:16 竖版、大字少字、强钩子、系列化版式 |
|
||||
|
||||
示例确认话术:
|
||||
|
||||
```text
|
||||
我建议用 A,因为这篇是活动攻略,读者最需要快速看懂门槛、封顶和翻车点。
|
||||
|
||||
A. warm-fintech-guide(推荐):白底暖橙、猫咪向导、金融教程感,适合 MAOMOMO 常规文章。
|
||||
B. data-card-dashboard:更强调数字卡片和计算表,适合门槛 / 返现 / 费用信息密集的文章。
|
||||
C. handdrawn-note:便签和手绘箭头更明显,适合避坑经验和保姆式步骤。
|
||||
|
||||
你选哪个?也可以指定颜色、猫咪比例或是否更像小红书图。
|
||||
```
|
||||
|
||||
## 常见配图类型
|
||||
|
||||
| 类型 | 用途 |
|
||||
|
|
@ -29,10 +63,10 @@
|
|||
生成图片前先规划清单:
|
||||
|
||||
```markdown
|
||||
| 文件名 | 类型 | 放置位置 | 核心文案 | alt text | 依赖素材 |
|
||||
|---|---|---|---|---|---|
|
||||
| hong-kong-bank-account-01-cover.png | 封面 | 标题下方 | 港卡保号关键点 | MAOMOMO 港卡保号教程封面图 | 无 |
|
||||
| hong-kong-bank-account-02-flow.png | 流程图 | 操作步骤前 | 三步完成账户活跃 | 港卡保号操作流程图 | 用户截图 1 |
|
||||
| 文件名 | 类型 | 放置位置 | 核心文案 | alt text | 风格 | 依赖素材 |
|
||||
|---|---|---|---|---|---|---|
|
||||
| hong-kong-bank-account-01-cover.png | 封面 | 标题下方 | 港卡保号关键点 | MAOMOMO 港卡保号教程封面图 | warm-fintech-guide | 无 |
|
||||
| hong-kong-bank-account-02-flow.png | 流程图 | 操作步骤前 | 三步完成账户活跃 | 港卡保号操作流程图 | handdrawn-note | 用户截图 1 |
|
||||
```
|
||||
|
||||
确认用户不需要先看清单时,可以直接执行,但最终必须让文件名、alt text 和插图位置与正文一致。
|
||||
|
|
@ -117,6 +151,7 @@ MAOMOMO 风格中文金融教程信息图,干净明亮白色背景,暖橙色
|
|||
- 每张图有 MAOMOMO 标识或猫咪水印。
|
||||
- 手机端可读,大字少字。
|
||||
- 默认结构:封面、核心拆解、操作流程、避坑总结。
|
||||
- 风格固定为 `xiaohongshu-vertical`,不直接复用横版文章图。
|
||||
|
||||
## 图片 QA
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,373 @@
|
|||
#!/usr/bin/env python3
|
||||
"""MAOMOMO article image generation CLI.
|
||||
|
||||
This is a lightweight OpenAI-compatible fallback for article assets. Prefer the
|
||||
agent's built-in image tool when it is available; use this script when the user
|
||||
explicitly wants API/CLI generation or the built-in backend is unavailable.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import base64
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import time
|
||||
from typing import Any, Dict, Iterable, List, Optional
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
|
||||
|
||||
DEFAULT_BASE_URL = "https://api.openai.com/v1"
|
||||
DEFAULT_MODEL = "gpt-image-2"
|
||||
DEFAULT_SIZE = "1536x1024"
|
||||
DEFAULT_QUALITY = "medium"
|
||||
DEFAULT_STYLE = "warm-fintech-guide"
|
||||
|
||||
|
||||
STYLE_PRESETS: Dict[str, str] = {
|
||||
"warm-fintech-guide": (
|
||||
"MAOMOMO 暖橙金融教程风:明亮白底,暖橙强调色,干净卡片布局,友好的白橘猫作为向导,"
|
||||
"适合港卡、银行活动、返现攻略和教程总览。"
|
||||
),
|
||||
"clean-editorial": (
|
||||
"清爽编辑部风:大标题、留白充足、少量暖橙和黑灰文字,像一张信息密度适中的中文攻略头图。"
|
||||
),
|
||||
"data-card-dashboard": (
|
||||
"数据卡片仪表盘风:指标卡、时间线、计算公式和对比表清晰分区,适合返现、费用、门槛和路径对比。"
|
||||
),
|
||||
"handdrawn-note": (
|
||||
"手绘便签风:白底纸感、手绘箭头、便利贴、重点圈注和轻量猫咪贴纸,适合避坑经验和保姆式步骤。"
|
||||
),
|
||||
"xiaohongshu-vertical": (
|
||||
"小红书竖版攻略风:9:16 手机端可读,大字少字,强标题钩子,4 图系列一致视觉,带 MAOMOMO 标识。"
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def die(message: str, code: int = 1) -> None:
|
||||
print(f"错误:{message}", file=sys.stderr)
|
||||
raise SystemExit(code)
|
||||
|
||||
|
||||
def warn(message: str) -> None:
|
||||
print(f"警告:{message}", file=sys.stderr)
|
||||
|
||||
|
||||
def read_text(path: Path) -> str:
|
||||
return path.read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def write_text(path: Path, text: str) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with path.open("w", encoding="utf-8", newline="\n") as handle:
|
||||
handle.write(text)
|
||||
|
||||
|
||||
def read_json(path: Path) -> Any:
|
||||
return json.loads(read_text(path))
|
||||
|
||||
|
||||
def load_codex_config_base_url() -> Optional[str]:
|
||||
config_path = Path.home() / ".codex" / "config.toml"
|
||||
if not config_path.exists():
|
||||
return None
|
||||
try:
|
||||
import tomllib
|
||||
except ModuleNotFoundError:
|
||||
return None
|
||||
try:
|
||||
data = tomllib.loads(read_text(config_path))
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
provider_name = data.get("model_provider")
|
||||
providers = data.get("model_providers")
|
||||
if isinstance(provider_name, str) and isinstance(providers, dict):
|
||||
provider = providers.get(provider_name)
|
||||
if isinstance(provider, dict) and isinstance(provider.get("base_url"), str):
|
||||
return provider["base_url"].strip()
|
||||
|
||||
if isinstance(data.get("base_url"), str):
|
||||
return data["base_url"].strip()
|
||||
if isinstance(providers, dict):
|
||||
for provider in providers.values():
|
||||
if isinstance(provider, dict) and isinstance(provider.get("base_url"), str):
|
||||
return provider["base_url"].strip()
|
||||
return None
|
||||
|
||||
|
||||
def find_secret(value: Any) -> Optional[str]:
|
||||
if isinstance(value, dict):
|
||||
for key in ("MAOMOMO_IMAGE_API_KEY", "OPENAI_API_KEY", "api_key", "openai_api_key", "token"):
|
||||
raw = value.get(key)
|
||||
if isinstance(raw, str) and raw.strip():
|
||||
return raw.strip()
|
||||
for child in value.values():
|
||||
found = find_secret(child)
|
||||
if found:
|
||||
return found
|
||||
if isinstance(value, list):
|
||||
for child in value:
|
||||
found = find_secret(child)
|
||||
if found:
|
||||
return found
|
||||
return None
|
||||
|
||||
|
||||
def load_codex_auth_api_key() -> Optional[str]:
|
||||
auth_path = Path.home() / ".codex" / "auth.json"
|
||||
if not auth_path.exists():
|
||||
return None
|
||||
try:
|
||||
return find_secret(read_json(auth_path))
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def api_key() -> str:
|
||||
value = os.getenv("MAOMOMO_IMAGE_API_KEY") or os.getenv("OPENAI_API_KEY") or load_codex_auth_api_key()
|
||||
if not value:
|
||||
die(
|
||||
"未找到 API Key。请设置 MAOMOMO_IMAGE_API_KEY 或 OPENAI_API_KEY,"
|
||||
"或确保 ~/.codex/auth.json 中存在可用 key。"
|
||||
)
|
||||
return value
|
||||
|
||||
|
||||
def base_url() -> str:
|
||||
return (
|
||||
os.getenv("MAOMOMO_IMAGE_BASE_URL")
|
||||
or os.getenv("OPENAI_BASE_URL")
|
||||
or load_codex_config_base_url()
|
||||
or DEFAULT_BASE_URL
|
||||
).rstrip("/")
|
||||
|
||||
|
||||
def image_model() -> str:
|
||||
return os.getenv("MAOMOMO_IMAGE_MODEL") or os.getenv("CODEX_PPT_IMAGE_MODEL") or DEFAULT_MODEL
|
||||
|
||||
|
||||
def style_prompt(style: str) -> str:
|
||||
if style in STYLE_PRESETS:
|
||||
return STYLE_PRESETS[style]
|
||||
return style
|
||||
|
||||
|
||||
def build_prompt(
|
||||
*,
|
||||
title: str,
|
||||
image_type: str,
|
||||
core_text: str,
|
||||
style: str,
|
||||
aspect_ratio: str,
|
||||
extra: str = "",
|
||||
) -> str:
|
||||
parts = [
|
||||
"生成一张 MAOMOMO 中文金融实操文章配图。",
|
||||
f"图片类型:{image_type}",
|
||||
f"标题 / 主题:{title}",
|
||||
f"核心文案:{core_text}",
|
||||
f"画幅:{aspect_ratio}",
|
||||
f"视觉风格:{style_prompt(style)}",
|
||||
"硬性要求:包含清晰可见的 MAOMOMO 标识;中文文字清楚可读;信息层级明确;不要伪造真实 App 截图;不要使用未经提供的银行、券商、支付机构或卡组织 Logo;不要出现真实个人信息;猫咪不能承载第三方品牌 Logo。",
|
||||
]
|
||||
if extra.strip():
|
||||
parts.append(f"补充要求:{extra.strip()}")
|
||||
return "\n".join(parts)
|
||||
|
||||
|
||||
def request_image(prompt: str, *, size: str, quality: str, output_format: str) -> bytes:
|
||||
payload = {
|
||||
"model": image_model(),
|
||||
"prompt": prompt,
|
||||
"size": size,
|
||||
"quality": quality,
|
||||
"n": 1,
|
||||
"response_format": "b64_json",
|
||||
}
|
||||
if output_format:
|
||||
payload["output_format"] = output_format
|
||||
|
||||
body = json.dumps(payload, ensure_ascii=False).encode("utf-8")
|
||||
req = urllib.request.Request(
|
||||
base_url() + "/images/generations",
|
||||
data=body,
|
||||
headers={
|
||||
"Authorization": f"Bearer {api_key()}",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
method="POST",
|
||||
)
|
||||
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=180) as resp:
|
||||
data = json.loads(resp.read().decode("utf-8"))
|
||||
except urllib.error.HTTPError as exc:
|
||||
detail = exc.read().decode("utf-8", "replace")
|
||||
die(f"图片接口返回 HTTP {exc.code}: {detail}")
|
||||
except Exception as exc:
|
||||
die(f"图片接口请求失败:{exc}")
|
||||
|
||||
items = data.get("data")
|
||||
if not isinstance(items, list) or not items:
|
||||
die("图片接口响应缺少 data。")
|
||||
first = items[0]
|
||||
if not isinstance(first, dict):
|
||||
die("图片接口响应格式不正确。")
|
||||
if isinstance(first.get("b64_json"), str):
|
||||
return base64.b64decode(first["b64_json"])
|
||||
if isinstance(first.get("url"), str):
|
||||
with urllib.request.urlopen(first["url"], timeout=180) as resp:
|
||||
return resp.read()
|
||||
die("图片接口响应中没有 b64_json 或 url。")
|
||||
return b""
|
||||
|
||||
|
||||
def normalize_output(path: str, *, base_dir: Optional[Path]) -> Path:
|
||||
out = Path(path)
|
||||
if not out.is_absolute() and base_dir is not None:
|
||||
out = base_dir / out
|
||||
return out.resolve()
|
||||
|
||||
|
||||
def generate_one(args: argparse.Namespace) -> Dict[str, str]:
|
||||
if args.prompt_file:
|
||||
prompt = read_text(Path(args.prompt_file)).strip()
|
||||
elif args.prompt:
|
||||
prompt = args.prompt.strip()
|
||||
else:
|
||||
prompt = build_prompt(
|
||||
title=args.title,
|
||||
image_type=args.image_type,
|
||||
core_text=args.core_text,
|
||||
style=args.style,
|
||||
aspect_ratio=args.aspect_ratio,
|
||||
extra=args.extra,
|
||||
)
|
||||
|
||||
out = normalize_output(args.out, base_dir=None)
|
||||
if args.dry_run:
|
||||
write_text(out.with_suffix(".prompt.txt"), prompt + "\n")
|
||||
return {"out": str(out), "prompt": str(out.with_suffix(".prompt.txt")), "status": "dry-run"}
|
||||
|
||||
image_bytes = request_image(prompt, size=args.size, quality=args.quality, output_format=args.output_format)
|
||||
out.parent.mkdir(parents=True, exist_ok=True)
|
||||
out.write_bytes(image_bytes)
|
||||
return {"out": str(out), "status": "generated"}
|
||||
|
||||
|
||||
def manifest_items(path: Path) -> List[Dict[str, Any]]:
|
||||
data = read_json(path)
|
||||
if isinstance(data, dict):
|
||||
items = data.get("images")
|
||||
else:
|
||||
items = data
|
||||
if not isinstance(items, list):
|
||||
die("manifest 必须是图片数组,或包含 images 数组的对象。")
|
||||
normalized: List[Dict[str, Any]] = []
|
||||
for index, item in enumerate(items, start=1):
|
||||
if not isinstance(item, dict):
|
||||
die(f"manifest 第 {index} 项不是对象。")
|
||||
normalized.append(item)
|
||||
return normalized
|
||||
|
||||
|
||||
def batch(args: argparse.Namespace) -> List[Dict[str, str]]:
|
||||
manifest_path = Path(args.manifest).resolve()
|
||||
base_dir = Path(args.base_dir).resolve() if args.base_dir else manifest_path.parent
|
||||
results: List[Dict[str, str]] = []
|
||||
|
||||
for index, item in enumerate(manifest_items(manifest_path), start=1):
|
||||
filename = item.get("file_name") or item.get("filename") or item.get("out")
|
||||
if not isinstance(filename, str) or not filename.strip():
|
||||
die(f"manifest 第 {index} 项缺少 file_name。")
|
||||
style = str(item.get("style") or args.style or DEFAULT_STYLE)
|
||||
aspect_ratio = str(item.get("aspect_ratio") or args.aspect_ratio)
|
||||
prompt = item.get("prompt")
|
||||
if not isinstance(prompt, str) or not prompt.strip():
|
||||
prompt = build_prompt(
|
||||
title=str(item.get("title") or item.get("alt_text") or filename),
|
||||
image_type=str(item.get("type") or "文章配图"),
|
||||
core_text=str(item.get("core_text") or item.get("caption") or item.get("alt_text") or ""),
|
||||
style=style,
|
||||
aspect_ratio=aspect_ratio,
|
||||
extra=str(item.get("extra") or item.get("requirements") or ""),
|
||||
)
|
||||
|
||||
out = normalize_output(filename, base_dir=base_dir)
|
||||
if args.dry_run:
|
||||
write_text(out.with_suffix(".prompt.txt"), prompt + "\n")
|
||||
results.append({"out": str(out), "prompt": str(out.with_suffix(".prompt.txt")), "status": "dry-run"})
|
||||
continue
|
||||
|
||||
image_bytes = request_image(
|
||||
prompt,
|
||||
size=str(item.get("size") or args.size),
|
||||
quality=str(item.get("quality") or args.quality),
|
||||
output_format=str(item.get("output_format") or args.output_format),
|
||||
)
|
||||
out.parent.mkdir(parents=True, exist_ok=True)
|
||||
out.write_bytes(image_bytes)
|
||||
results.append({"out": str(out), "status": "generated"})
|
||||
if args.sleep > 0:
|
||||
time.sleep(args.sleep)
|
||||
return results
|
||||
|
||||
|
||||
def print_styles(_: argparse.Namespace) -> int:
|
||||
print(json.dumps(STYLE_PRESETS, ensure_ascii=False, indent=2))
|
||||
return 0
|
||||
|
||||
|
||||
def build_parser() -> argparse.ArgumentParser:
|
||||
parser = argparse.ArgumentParser(description="Generate MAOMOMO article images with an OpenAI-compatible API.")
|
||||
subparsers = parser.add_subparsers(dest="command", required=True)
|
||||
|
||||
common = argparse.ArgumentParser(add_help=False)
|
||||
common.add_argument("--style", default=DEFAULT_STYLE, help="Style preset name or custom style text.")
|
||||
common.add_argument("--size", default=DEFAULT_SIZE)
|
||||
common.add_argument("--quality", default=DEFAULT_QUALITY)
|
||||
common.add_argument("--output-format", default="png")
|
||||
common.add_argument("--aspect-ratio", default="16:9")
|
||||
common.add_argument("--dry-run", action="store_true", help="Only write prompt files; do not call the image API.")
|
||||
|
||||
generate = subparsers.add_parser("generate", parents=[common], help="Generate one image.")
|
||||
generate.add_argument("--out", required=True)
|
||||
generate.add_argument("--prompt")
|
||||
generate.add_argument("--prompt-file")
|
||||
generate.add_argument("--title", default="MAOMOMO 文章配图")
|
||||
generate.add_argument("--image-type", default="文章配图")
|
||||
generate.add_argument("--core-text", default="")
|
||||
generate.add_argument("--extra", default="")
|
||||
|
||||
batch_parser = subparsers.add_parser("batch", parents=[common], help="Generate images from a JSON manifest.")
|
||||
batch_parser.add_argument("--manifest", required=True)
|
||||
batch_parser.add_argument("--base-dir", help="Base directory for relative file_name paths. Defaults to manifest dir.")
|
||||
batch_parser.add_argument("--sleep", type=float, default=0.0, help="Seconds to sleep between API calls.")
|
||||
|
||||
subparsers.add_parser("styles", help="Print bundled style presets.")
|
||||
return parser
|
||||
|
||||
|
||||
def main(argv: Optional[Iterable[str]] = None) -> int:
|
||||
parser = build_parser()
|
||||
args = parser.parse_args(list(argv) if argv is not None else None)
|
||||
if args.command == "styles":
|
||||
return print_styles(args)
|
||||
if args.command == "generate":
|
||||
result = generate_one(args)
|
||||
print(json.dumps(result, ensure_ascii=False, indent=2))
|
||||
return 0
|
||||
if args.command == "batch":
|
||||
results = batch(args)
|
||||
print(json.dumps(results, ensure_ascii=False, indent=2))
|
||||
return 0
|
||||
parser.print_help()
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Loading…
Reference in New Issue