mirror of
https://github.com/JimLiu/baoyu-skills.git
synced 2026-07-13 22:29:48 +08:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 715555c2ab | |||
| 02b039f2ff | |||
| 00306414fe | |||
| db1179e057 | |||
| 7b0c9ca372 |
@@ -6,7 +6,7 @@
|
|||||||
},
|
},
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"description": "Skills shared by Baoyu for improving daily work efficiency",
|
"description": "Skills shared by Baoyu for improving daily work efficiency",
|
||||||
"version": "1.20.0"
|
"version": "1.21.2"
|
||||||
},
|
},
|
||||||
"plugins": [
|
"plugins": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -186,18 +186,61 @@ Japanese (CHANGELOG.ja.md):
|
|||||||
- コネクションプールのメモリリークを修正
|
- コネクションプールのメモリリークを修正
|
||||||
```
|
```
|
||||||
|
|
||||||
### Step 5: Check README Updates
|
### Step 5: Group Changes by Skill/Module
|
||||||
|
|
||||||
**IMPORTANT**: Before updating version file, check if README needs updates.
|
Analyze commits since last tag and group by affected skill/module:
|
||||||
|
|
||||||
1. **Scan for README files**: `README*.md` in project root
|
1. **Identify changed files** per commit
|
||||||
2. **For each skill/feature changed**, check if README documentation matches:
|
2. **Group by skill/module**:
|
||||||
- Options/flags documented correctly
|
- `skills/<skill-name>/*` → Group under that skill
|
||||||
- Usage examples reflect current syntax
|
- Root files (CLAUDE.md, etc.) → Group as "project"
|
||||||
- Feature descriptions are accurate
|
- Multiple skills in one commit → Split into multiple groups
|
||||||
3. **If README is outdated**:
|
3. **For each group**, identify related README updates needed
|
||||||
- Update README.md (and README.zh.md if exists) to reflect changes
|
|
||||||
- Include in release commit
|
**Example Grouping**:
|
||||||
|
```
|
||||||
|
baoyu-cover-image:
|
||||||
|
- feat: add new style options
|
||||||
|
- fix: handle transparent backgrounds
|
||||||
|
→ README updates: options table
|
||||||
|
|
||||||
|
baoyu-comic:
|
||||||
|
- refactor: improve panel layout algorithm
|
||||||
|
→ No README updates needed
|
||||||
|
|
||||||
|
project:
|
||||||
|
- docs: update CLAUDE.md architecture section
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 6: Commit Each Skill/Module Separately
|
||||||
|
|
||||||
|
For each skill/module group (in order of changes):
|
||||||
|
|
||||||
|
1. **Check README updates needed**:
|
||||||
|
- Scan `README*.md` for mentions of this skill/module
|
||||||
|
- Verify options/flags documented correctly
|
||||||
|
- Update usage examples if syntax changed
|
||||||
|
- Update feature descriptions if behavior changed
|
||||||
|
|
||||||
|
2. **Stage and commit**:
|
||||||
|
```bash
|
||||||
|
git add skills/<skill-name>/*
|
||||||
|
git add README.md README.zh.md # If updated for this skill
|
||||||
|
git commit -m "<type>(<skill-name>): <meaningful description>"
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Commit message format**:
|
||||||
|
- Use conventional commit format: `<type>(<scope>): <description>`
|
||||||
|
- `<type>`: feat, fix, refactor, docs, perf, etc.
|
||||||
|
- `<scope>`: skill name or "project"
|
||||||
|
- `<description>`: Clear, meaningful description of changes
|
||||||
|
|
||||||
|
**Example Commits**:
|
||||||
|
```bash
|
||||||
|
git commit -m "feat(baoyu-cover-image): add watercolor and minimalist styles"
|
||||||
|
git commit -m "fix(baoyu-comic): improve panel layout for long dialogues"
|
||||||
|
git commit -m "docs(project): update architecture documentation"
|
||||||
|
```
|
||||||
|
|
||||||
**Common README Updates Needed**:
|
**Common README Updates Needed**:
|
||||||
| Change Type | README Section to Check |
|
| Change Type | README Section to Check |
|
||||||
@@ -208,11 +251,13 @@ Japanese (CHANGELOG.ja.md):
|
|||||||
| Breaking changes | Migration notes, deprecation warnings |
|
| Breaking changes | Migration notes, deprecation warnings |
|
||||||
| Restructured internals | Architecture section (if exposed to users) |
|
| Restructured internals | Architecture section (if exposed to users) |
|
||||||
|
|
||||||
### Step 6: Update Version File
|
### Step 7: Generate Changelog and Update Version
|
||||||
|
|
||||||
1. Read version file (JSON/TOML/text)
|
1. **Generate multi-language changelogs** (as described in Step 4)
|
||||||
2. Update version number
|
2. **Update version file**:
|
||||||
3. Write back (preserve formatting)
|
- Read version file (JSON/TOML/text)
|
||||||
|
- Update version number
|
||||||
|
- Write back (preserve formatting)
|
||||||
|
|
||||||
**Version Paths by File Type**:
|
**Version Paths by File Type**:
|
||||||
|
|
||||||
@@ -224,25 +269,77 @@ Japanese (CHANGELOG.ja.md):
|
|||||||
| marketplace.json | `$.metadata.version` |
|
| marketplace.json | `$.metadata.version` |
|
||||||
| VERSION / version.txt | Direct content |
|
| VERSION / version.txt | Direct content |
|
||||||
|
|
||||||
### Step 7: Commit and Tag
|
### Step 8: User Confirmation
|
||||||
|
|
||||||
```bash
|
Before creating the release commit, ask user to confirm:
|
||||||
git add <all modified files>
|
|
||||||
git commit -m "chore: release v{VERSION}"
|
**Use AskUserQuestion with two questions**:
|
||||||
git tag v{VERSION}
|
|
||||||
|
1. **Version bump** (single select):
|
||||||
|
- Show recommended version based on Step 3 analysis
|
||||||
|
- Options: recommended (with label), other semver options
|
||||||
|
- Example: `1.2.3 → 1.3.0 (Recommended)`, `1.2.3 → 1.2.4`, `1.2.3 → 2.0.0`
|
||||||
|
|
||||||
|
2. **Push to remote** (single select):
|
||||||
|
- Options: "Yes, push after commit", "No, keep local only"
|
||||||
|
|
||||||
|
**Example Output Before Confirmation**:
|
||||||
```
|
```
|
||||||
|
Commits created:
|
||||||
|
1. feat(baoyu-cover-image): add watercolor and minimalist styles
|
||||||
|
2. fix(baoyu-comic): improve panel layout for long dialogues
|
||||||
|
3. docs(project): update architecture documentation
|
||||||
|
|
||||||
|
Changelog preview (en):
|
||||||
|
## 1.3.0 - 2026-01-22
|
||||||
|
### Features
|
||||||
|
- Add watercolor and minimalist styles to cover-image
|
||||||
|
### Fixes
|
||||||
|
- Improve panel layout for long dialogues in comic
|
||||||
|
|
||||||
|
Ready to create release commit and tag.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 9: Create Release Commit and Tag
|
||||||
|
|
||||||
|
After user confirmation:
|
||||||
|
|
||||||
|
1. **Stage version and changelog files**:
|
||||||
|
```bash
|
||||||
|
git add <version-file>
|
||||||
|
git add CHANGELOG*.md
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Create release commit**:
|
||||||
|
```bash
|
||||||
|
git commit -m "chore: release v{VERSION}"
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Create tag**:
|
||||||
|
```bash
|
||||||
|
git tag v{VERSION}
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Push if user confirmed** (Step 8):
|
||||||
|
```bash
|
||||||
|
git push origin main
|
||||||
|
git push origin v{VERSION}
|
||||||
|
```
|
||||||
|
|
||||||
**Note**: Do NOT add Co-Authored-By line. This is a release commit, not a code contribution.
|
**Note**: Do NOT add Co-Authored-By line. This is a release commit, not a code contribution.
|
||||||
|
|
||||||
**Important**: Do NOT push to remote. User will push manually when ready.
|
|
||||||
|
|
||||||
**Post-Release Output**:
|
**Post-Release Output**:
|
||||||
```
|
```
|
||||||
Release v1.3.0 created locally.
|
Release v1.3.0 created.
|
||||||
|
|
||||||
To publish:
|
Commits:
|
||||||
git push origin main
|
1. feat(baoyu-cover-image): add watercolor and minimalist styles
|
||||||
git push origin v1.3.0
|
2. fix(baoyu-comic): improve panel layout for long dialogues
|
||||||
|
3. docs(project): update architecture documentation
|
||||||
|
4. chore: release v1.3.0
|
||||||
|
|
||||||
|
Tag: v1.3.0
|
||||||
|
Status: Pushed to origin # or "Local only - run git push when ready"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Configuration (.releaserc.yml)
|
## Configuration (.releaserc.yml)
|
||||||
@@ -307,31 +404,36 @@ Project detected:
|
|||||||
Last tag: v1.2.3
|
Last tag: v1.2.3
|
||||||
Proposed version: v1.3.0
|
Proposed version: v1.3.0
|
||||||
|
|
||||||
Changes detected:
|
Changes grouped by skill/module:
|
||||||
feat: add user authentication
|
baoyu-cover-image:
|
||||||
feat: support oauth2 login
|
- feat: add watercolor style
|
||||||
fix: memory leak in pool
|
- feat: add minimalist style
|
||||||
|
→ Commit: feat(baoyu-cover-image): add watercolor and minimalist styles
|
||||||
|
→ README updates: options table
|
||||||
|
|
||||||
|
baoyu-comic:
|
||||||
|
- fix: panel layout for long dialogues
|
||||||
|
→ Commit: fix(baoyu-comic): improve panel layout for long dialogues
|
||||||
|
→ No README updates
|
||||||
|
|
||||||
Changelog preview (en):
|
Changelog preview (en):
|
||||||
## 1.3.0 - 2026-01-22
|
## 1.3.0 - 2026-01-22
|
||||||
### Features
|
### Features
|
||||||
- Add user authentication module
|
- Add watercolor and minimalist styles to cover-image
|
||||||
- Support OAuth2 login
|
|
||||||
### Fixes
|
### Fixes
|
||||||
- Fix memory leak in connection pool
|
- Improve panel layout for long dialogues in comic
|
||||||
|
|
||||||
Changelog preview (zh):
|
Changelog preview (zh):
|
||||||
## 1.3.0 - 2026-01-22
|
## 1.3.0 - 2026-01-22
|
||||||
### 新功能
|
### 新功能
|
||||||
- 新增用户认证模块
|
- 为 cover-image 添加水彩和极简风格
|
||||||
- 支持 OAuth2 登录
|
|
||||||
### 修复
|
### 修复
|
||||||
- 修复连接池内存泄漏问题
|
- 改进 comic 长对话的面板布局
|
||||||
|
|
||||||
Files to modify:
|
Commits to create:
|
||||||
- package.json
|
1. feat(baoyu-cover-image): add watercolor and minimalist styles
|
||||||
- CHANGELOG.md
|
2. fix(baoyu-comic): improve panel layout for long dialogues
|
||||||
- CHANGELOG.zh.md
|
3. chore: release v1.3.0
|
||||||
|
|
||||||
No changes made. Run without --dry-run to execute.
|
No changes made. Run without --dry-run to execute.
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -2,6 +2,25 @@
|
|||||||
|
|
||||||
English | [中文](./CHANGELOG.zh.md)
|
English | [中文](./CHANGELOG.zh.md)
|
||||||
|
|
||||||
|
## 1.21.2 - 2026-01-24
|
||||||
|
|
||||||
|
### Features
|
||||||
|
- `baoyu-image-gen`: adds parallel generation documentation with recommended 4 concurrent subagents for batch operations.
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
- `release-skills`: adds skill/module grouping workflow and user confirmation step before release.
|
||||||
|
|
||||||
|
## 1.21.1 - 2026-01-24
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
- `baoyu-comic`: adds character sheet compression step after generation to reduce token usage when used as reference image.
|
||||||
|
|
||||||
|
## 1.21.0 - 2026-01-24
|
||||||
|
|
||||||
|
### Features
|
||||||
|
- `baoyu-cover-image`: expands aspect ratio options—adds 4:3, 3:2, 3:4 ratios; changes default from 2.35:1 to 16:9 for better versatility. Aspect ratio is now always confirmed unless explicitly specified via `--aspect` flag.
|
||||||
|
- `baoyu-image-gen`: refactors Google provider to support both Gemini multimodal and Imagen models with unified API. Adds `--imageSize` parameter support (1K/2K/4K) for Gemini models.
|
||||||
|
|
||||||
## 1.20.0 - 2026-01-24
|
## 1.20.0 - 2026-01-24
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|||||||
@@ -2,6 +2,25 @@
|
|||||||
|
|
||||||
[English](./CHANGELOG.md) | 中文
|
[English](./CHANGELOG.md) | 中文
|
||||||
|
|
||||||
|
## 1.21.2 - 2026-01-24
|
||||||
|
|
||||||
|
### 新功能
|
||||||
|
- `baoyu-image-gen`:添加并行生成文档,推荐使用 4 个并发 subagent 进行批量操作。
|
||||||
|
|
||||||
|
### 文档
|
||||||
|
- `release-skills`:新增按 skill/module 分组提交流程和发布前用户确认步骤。
|
||||||
|
|
||||||
|
## 1.21.1 - 2026-01-24
|
||||||
|
|
||||||
|
### 文档
|
||||||
|
- `baoyu-comic`:在角色参考图生成后添加压缩步骤,减少作为参考图使用时的 token 消耗。
|
||||||
|
|
||||||
|
## 1.21.0 - 2026-01-24
|
||||||
|
|
||||||
|
### 新功能
|
||||||
|
- `baoyu-cover-image`:扩展宽高比选项——新增 4:3、3:2、3:4 比例;默认值从 2.35:1 改为 16:9 以提高通用性。现在除非通过 `--aspect` 标志明确指定,否则始终确认宽高比。
|
||||||
|
- `baoyu-image-gen`:重构 Google provider 以统一支持 Gemini 多模态和 Imagen 模型。为 Gemini 模型新增 `--imageSize` 参数支持(1K/2K/4K)。
|
||||||
|
|
||||||
## 1.20.0 - 2026-01-24
|
## 1.20.0 - 2026-01-24
|
||||||
|
|
||||||
### 新功能
|
### 新功能
|
||||||
|
|||||||
@@ -201,6 +201,12 @@ npx -y bun ${SKILL_DIR}/../baoyu-image-gen/scripts/main.ts \
|
|||||||
--image characters/characters.png --ar 4:3
|
--image characters/characters.png --ar 4:3
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Compress character sheet** (recommended):
|
||||||
|
Compress to reduce token usage when used as reference image:
|
||||||
|
- Use available image compression skill (if any)
|
||||||
|
- Or system tools: `pngquant`, `optipng`, `sips` (macOS)
|
||||||
|
- **Keep PNG format**, lossless compression preferred
|
||||||
|
|
||||||
**7.2 Generate each page WITH character reference**:
|
**7.2 Generate each page WITH character reference**:
|
||||||
|
|
||||||
| Skill Capability | Strategy |
|
| Skill Capability | Strategy |
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ Generate elegant cover images for articles with 4-dimensional customization.
|
|||||||
| `--style <name>` | Cover style (see Style Gallery) |
|
| `--style <name>` | Cover style (see Style Gallery) |
|
||||||
| `--text <level>` | Text density: none, title-only, title-subtitle, text-rich |
|
| `--text <level>` | Text density: none, title-only, title-subtitle, text-rich |
|
||||||
| `--mood <level>` | Emotional intensity: subtle, balanced, bold |
|
| `--mood <level>` | Emotional intensity: subtle, balanced, bold |
|
||||||
| `--aspect <ratio>` | 2.35:1 (default), 16:9, 1:1 |
|
| `--aspect <ratio>` | 16:9 (default), 2.35:1, 4:3, 3:2, 1:1, 3:4 |
|
||||||
| `--lang <code>` | Title language (en, zh, ja, etc.) |
|
| `--lang <code>` | Title language (en, zh, ja, etc.) |
|
||||||
| `--no-title` | Alias for `--text none` |
|
| `--no-title` | Alias for `--text none` |
|
||||||
| `--quick` | Skip confirmation, use auto-selection for missing dimensions |
|
| `--quick` | Skip confirmation, use auto-selection for missing dimensions |
|
||||||
@@ -357,14 +357,18 @@ options:
|
|||||||
header: "Aspect"
|
header: "Aspect"
|
||||||
question: "Default aspect ratio for cover images?"
|
question: "Default aspect ratio for cover images?"
|
||||||
options:
|
options:
|
||||||
- label: "2.35:1 (Recommended)"
|
- label: "16:9 (Recommended)"
|
||||||
description: "Cinematic widescreen, best for article headers"
|
description: "Standard widescreen - YouTube, presentations, versatile"
|
||||||
- label: "16:9"
|
- label: "2.35:1"
|
||||||
description: "Standard widescreen, versatile"
|
description: "Cinematic widescreen - article headers, blog posts"
|
||||||
- label: "1:1"
|
- label: "1:1"
|
||||||
description: "Square, social media friendly"
|
description: "Square - Instagram, WeChat, social cards"
|
||||||
|
- label: "3:4"
|
||||||
|
description: "Portrait - Xiaohongshu, Pinterest, mobile content"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Note: More ratios (4:3, 3:2) available during generation. This sets the default recommendation.
|
||||||
|
|
||||||
**Q5: Quick Mode**
|
**Q5: Quick Mode**
|
||||||
```yaml
|
```yaml
|
||||||
header: "Quick"
|
header: "Quick"
|
||||||
@@ -415,17 +419,19 @@ Read source content, save it if needed, and perform analysis.
|
|||||||
|
|
||||||
### Step 2: Confirm Options ⚠️
|
### Step 2: Confirm Options ⚠️
|
||||||
|
|
||||||
**Purpose**: Validate all 4 dimensions. **Skip if `--quick` flag OR all 4 dimensions specified via CLI.**
|
**Purpose**: Validate all 4 dimensions + aspect ratio.
|
||||||
|
|
||||||
**Skip Conditions**:
|
**Skip Conditions**:
|
||||||
| Condition | Behavior |
|
| Condition | Skipped Questions | Still Asked |
|
||||||
|-----------|----------|
|
|-----------|-------------------|-------------|
|
||||||
| `--quick` flag | Auto-select missing dimensions, skip to Step 3 |
|
| `--quick` flag | Type, Style, Text, Mood | **Aspect Ratio** (unless `--aspect` specified) |
|
||||||
| All 4 dimensions specified | Use specified values, skip to Step 3 |
|
| All 4 dimensions + `--aspect` specified | All | None |
|
||||||
| `quick_mode: true` in EXTEND.md | Auto-select missing dimensions, skip to Step 3 |
|
| `quick_mode: true` in EXTEND.md | Type, Style, Text, Mood | **Aspect Ratio** (unless `--aspect` specified) |
|
||||||
| Otherwise | Show confirmation (current behavior) |
|
| Otherwise | None | All 5 questions |
|
||||||
|
|
||||||
**Quick Mode Output** (when skipping confirmation):
|
**Important**: Aspect ratio is ALWAYS asked unless explicitly specified via `--aspect` CLI flag. User presets in EXTEND.md are shown as recommended option, not auto-selected.
|
||||||
|
|
||||||
|
**Quick Mode Output** (when skipping 4 dimensions):
|
||||||
|
|
||||||
```
|
```
|
||||||
Quick Mode: Auto-selected dimensions
|
Quick Mode: Auto-selected dimensions
|
||||||
@@ -433,9 +439,8 @@ Quick Mode: Auto-selected dimensions
|
|||||||
• Style: [style] ([reason])
|
• Style: [style] ([reason])
|
||||||
• Text: [text] ([reason])
|
• Text: [text] ([reason])
|
||||||
• Mood: [mood] ([reason])
|
• Mood: [mood] ([reason])
|
||||||
• Aspect: [aspect]
|
|
||||||
|
|
||||||
Generating...
|
[Then ask Question 5: Aspect Ratio]
|
||||||
```
|
```
|
||||||
|
|
||||||
**Confirmation Flow** (when NOT skipping):
|
**Confirmation Flow** (when NOT skipping):
|
||||||
@@ -513,19 +518,27 @@ options:
|
|||||||
description: "High contrast, vivid colors, dynamic"
|
description: "High contrast, vivid colors, dynamic"
|
||||||
```
|
```
|
||||||
|
|
||||||
**Question 5: Aspect Ratio** (if not specified via `--aspect`)
|
**Question 5: Aspect Ratio** (ALWAYS ask unless `--aspect` specified via CLI)
|
||||||
|
|
||||||
|
Note: Even if user has a preset in EXTEND.md, still ask this question. The preset is shown as the recommended option.
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
header: "Aspect"
|
header: "Aspect"
|
||||||
question: "Cover aspect ratio?"
|
question: "Cover aspect ratio?"
|
||||||
multiSelect: false
|
multiSelect: false
|
||||||
options:
|
options:
|
||||||
- label: "2.35:1 (Recommended)"
|
- label: "[user preset or 16:9] (Recommended)"
|
||||||
description: "Cinematic widescreen, best for article headers"
|
description: "[based on preset or default: Standard widescreen, versatile]"
|
||||||
- label: "16:9"
|
- label: "2.35:1"
|
||||||
description: "Standard widescreen, versatile"
|
description: "Cinematic widescreen - article headers, blog posts"
|
||||||
|
- label: "4:3"
|
||||||
|
description: "Traditional screen - PPT slides, classic displays"
|
||||||
|
- label: "3:2"
|
||||||
|
description: "Photography ratio - blog articles, Medium posts"
|
||||||
- label: "1:1"
|
- label: "1:1"
|
||||||
description: "Square, social media friendly"
|
description: "Square - Instagram, WeChat moments, social cards"
|
||||||
|
- label: "3:4"
|
||||||
|
description: "Portrait - Xiaohongshu, Pinterest, mobile-first content"
|
||||||
```
|
```
|
||||||
|
|
||||||
**After response**: Proceed to Step 3 with confirmed dimensions.
|
**After response**: Proceed to Step 3 with confirmed dimensions.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: baoyu-image-gen
|
name: baoyu-image-gen
|
||||||
description: Generates images using official OpenAI and Google APIs via AI SDK. Supports text-to-image, reference images, aspect ratios, and quality presets. Use when user asks to "generate image with API", "use official API for images", "create image with OpenAI/Google", or needs API-based generation instead of browser-based.
|
description: AI image generation with OpenAI and Google APIs. Supports text-to-image, reference images, aspect ratios, and parallel generation (recommended 4 concurrent subagents). Use when user asks to generate, create, or draw images.
|
||||||
---
|
---
|
||||||
|
|
||||||
# Image Generation (AI SDK)
|
# Image Generation (AI SDK)
|
||||||
@@ -118,6 +118,25 @@ Supported: `1:1`, `16:9`, `9:16`, `4:3`, `3:4`, `2.35:1`
|
|||||||
- Google Imagen: uses `aspectRatio` parameter
|
- Google Imagen: uses `aspectRatio` parameter
|
||||||
- OpenAI: maps to closest supported size
|
- OpenAI: maps to closest supported size
|
||||||
|
|
||||||
|
## Parallel Generation
|
||||||
|
|
||||||
|
Supports concurrent image generation via background subagents for batch operations.
|
||||||
|
|
||||||
|
| Setting | Value |
|
||||||
|
|---------|-------|
|
||||||
|
| Recommended concurrency | 4 subagents |
|
||||||
|
| Max concurrency | 8 subagents |
|
||||||
|
| Use case | Batch generation (slides, comics, infographics) |
|
||||||
|
|
||||||
|
**Agent Implementation**:
|
||||||
|
```
|
||||||
|
# Launch multiple generations in parallel using Task tool
|
||||||
|
# Each Task runs as background subagent with run_in_background=true
|
||||||
|
# Collect results via TaskOutput when all complete
|
||||||
|
```
|
||||||
|
|
||||||
|
**Best Practice**: When generating 4+ images, spawn background subagents (recommended 4 concurrent) instead of sequential execution.
|
||||||
|
|
||||||
## Error Handling
|
## Error Handling
|
||||||
|
|
||||||
- Missing API key → error with setup instructions
|
- Missing API key → error with setup instructions
|
||||||
|
|||||||
@@ -2,19 +2,25 @@ import path from "node:path";
|
|||||||
import { readFile } from "node:fs/promises";
|
import { readFile } from "node:fs/promises";
|
||||||
import type { CliArgs } from "../types";
|
import type { CliArgs } from "../types";
|
||||||
|
|
||||||
const GOOGLE_MULTIMODAL_MODELS = ["gemini-3-pro-image-preview"];
|
const GOOGLE_MULTIMODAL_MODELS = ["gemini-3-pro-image-preview", "gemini-3-flash-preview"];
|
||||||
const GOOGLE_IMAGEN_MODELS = ["imagen-3.0-generate-002", "imagen-3.0-generate-001"];
|
const GOOGLE_IMAGEN_MODELS = ["imagen-3.0-generate-002", "imagen-3.0-generate-001"];
|
||||||
|
|
||||||
export function getDefaultModel(): string {
|
export function getDefaultModel(): string {
|
||||||
return process.env.GOOGLE_IMAGE_MODEL || "gemini-3-pro-image-preview";
|
return process.env.GOOGLE_IMAGE_MODEL || "gemini-3-pro-image-preview";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeGoogleModelId(model: string): string {
|
||||||
|
return model.startsWith("models/") ? model.slice("models/".length) : model;
|
||||||
|
}
|
||||||
|
|
||||||
function isGoogleMultimodal(model: string): boolean {
|
function isGoogleMultimodal(model: string): boolean {
|
||||||
return GOOGLE_MULTIMODAL_MODELS.some((m) => model.includes(m));
|
const normalized = normalizeGoogleModelId(model);
|
||||||
|
return GOOGLE_MULTIMODAL_MODELS.some((m) => normalized.includes(m));
|
||||||
}
|
}
|
||||||
|
|
||||||
function isGoogleImagen(model: string): boolean {
|
function isGoogleImagen(model: string): boolean {
|
||||||
return GOOGLE_IMAGEN_MODELS.some((m) => model.includes(m));
|
const normalized = normalizeGoogleModelId(model);
|
||||||
|
return GOOGLE_IMAGEN_MODELS.some((m) => normalized.includes(m));
|
||||||
}
|
}
|
||||||
|
|
||||||
function getGoogleApiKey(): string | null {
|
function getGoogleApiKey(): string | null {
|
||||||
@@ -26,6 +32,44 @@ function getGoogleImageSize(args: CliArgs): "1K" | "2K" | "4K" {
|
|||||||
return args.quality === "2k" ? "2K" : "1K";
|
return args.quality === "2k" ? "2K" : "1K";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getGoogleBaseUrl(): string {
|
||||||
|
const base = process.env.GOOGLE_BASE_URL || "https://generativelanguage.googleapis.com";
|
||||||
|
return base.replace(/\/+$/g, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildGoogleUrl(pathname: string): string {
|
||||||
|
const base = getGoogleBaseUrl();
|
||||||
|
const cleanedPath = pathname.replace(/^\/+/g, "");
|
||||||
|
if (base.endsWith("/v1beta")) return `${base}/${cleanedPath}`;
|
||||||
|
return `${base}/v1beta/${cleanedPath}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toModelPath(model: string): string {
|
||||||
|
const modelId = normalizeGoogleModelId(model);
|
||||||
|
return `models/${modelId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function postGoogleJson<T>(pathname: string, body: unknown): Promise<T> {
|
||||||
|
const apiKey = getGoogleApiKey();
|
||||||
|
if (!apiKey) throw new Error("GOOGLE_API_KEY or GEMINI_API_KEY is required");
|
||||||
|
|
||||||
|
const res = await fetch(buildGoogleUrl(pathname), {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"x-goog-api-key": apiKey,
|
||||||
|
},
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const err = await res.text();
|
||||||
|
throw new Error(`Google API error (${res.status}): ${err}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (await res.json()) as T;
|
||||||
|
}
|
||||||
|
|
||||||
function buildPromptWithAspect(prompt: string, ar: string | null, quality: CliArgs["quality"]): string {
|
function buildPromptWithAspect(prompt: string, ar: string | null, quality: CliArgs["quality"]): string {
|
||||||
let result = prompt;
|
let result = prompt;
|
||||||
if (ar) {
|
if (ar) {
|
||||||
@@ -37,6 +81,11 @@ function buildPromptWithAspect(prompt: string, ar: string | null, quality: CliAr
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function addAspectRatioToPrompt(prompt: string, ar: string | null): string {
|
||||||
|
if (!ar) return prompt;
|
||||||
|
return `${prompt} Aspect ratio: ${ar}.`;
|
||||||
|
}
|
||||||
|
|
||||||
async function readImageAsBase64(p: string): Promise<{ data: string; mimeType: string }> {
|
async function readImageAsBase64(p: string): Promise<{ data: string; mimeType: string }> {
|
||||||
const buf = await readFile(p);
|
const buf = await readFile(p);
|
||||||
const ext = path.extname(p).toLowerCase();
|
const ext = path.extname(p).toLowerCase();
|
||||||
@@ -47,53 +96,74 @@ async function readImageAsBase64(p: string): Promise<{ data: string; mimeType: s
|
|||||||
return { data: buf.toString("base64"), mimeType };
|
return { data: buf.toString("base64"), mimeType };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function extractInlineImageData(response: {
|
||||||
|
candidates?: Array<{ content?: { parts?: Array<{ inlineData?: { data?: string } }> } }>;
|
||||||
|
}): string | null {
|
||||||
|
for (const candidate of response.candidates || []) {
|
||||||
|
for (const part of candidate.content?.parts || []) {
|
||||||
|
const data = part.inlineData?.data;
|
||||||
|
if (typeof data === "string" && data.length > 0) return data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractPredictedImageData(response: {
|
||||||
|
predictions?: Array<any>;
|
||||||
|
generatedImages?: Array<any>;
|
||||||
|
}): string | null {
|
||||||
|
const candidates = [...(response.predictions || []), ...(response.generatedImages || [])];
|
||||||
|
for (const candidate of candidates) {
|
||||||
|
if (!candidate || typeof candidate !== "object") continue;
|
||||||
|
if (typeof candidate.imageBytes === "string") return candidate.imageBytes;
|
||||||
|
if (typeof candidate.bytesBase64Encoded === "string") return candidate.bytesBase64Encoded;
|
||||||
|
if (typeof candidate.data === "string") return candidate.data;
|
||||||
|
const image = candidate.image;
|
||||||
|
if (image && typeof image === "object") {
|
||||||
|
if (typeof image.imageBytes === "string") return image.imageBytes;
|
||||||
|
if (typeof image.bytesBase64Encoded === "string") return image.bytesBase64Encoded;
|
||||||
|
if (typeof image.data === "string") return image.data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
async function generateWithGemini(
|
async function generateWithGemini(
|
||||||
prompt: string,
|
prompt: string,
|
||||||
model: string,
|
model: string,
|
||||||
args: CliArgs
|
args: CliArgs
|
||||||
): Promise<Uint8Array> {
|
): Promise<Uint8Array> {
|
||||||
const { GoogleGenAI } = await import("@google/genai");
|
const promptWithAspect = addAspectRatioToPrompt(prompt, args.aspectRatio);
|
||||||
|
const parts: Array<{ text?: string; inlineData?: { data: string; mimeType: string } }> = [];
|
||||||
const apiKey = getGoogleApiKey();
|
|
||||||
if (!apiKey) throw new Error("GOOGLE_API_KEY or GEMINI_API_KEY is required");
|
|
||||||
|
|
||||||
const ai = new GoogleGenAI({
|
|
||||||
apiKey,
|
|
||||||
httpOptions: {
|
|
||||||
baseUrl: process.env.GOOGLE_BASE_URL || undefined,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const input: Array<{ type: "text" | "image"; text?: string; data?: string; mime_type?: string }> = [];
|
|
||||||
for (const refPath of args.referenceImages) {
|
for (const refPath of args.referenceImages) {
|
||||||
const { data, mimeType } = await readImageAsBase64(refPath);
|
const { data, mimeType } = await readImageAsBase64(refPath);
|
||||||
input.push({ type: "image", data, mime_type: mimeType });
|
parts.push({ inlineData: { data, mimeType } });
|
||||||
}
|
}
|
||||||
input.push({ type: "text", text: prompt });
|
parts.push({ text: promptWithAspect });
|
||||||
|
|
||||||
const imageConfig: { image_size: "1K" | "2K" | "4K"; aspect_ratio?: string } = {
|
const imageConfig: { imageSize: "1K" | "2K" | "4K" } = {
|
||||||
image_size: getGoogleImageSize(args),
|
imageSize: getGoogleImageSize(args),
|
||||||
};
|
};
|
||||||
if (args.aspectRatio) {
|
|
||||||
imageConfig.aspect_ratio = args.aspectRatio;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("Generating image with Gemini...", imageConfig);
|
console.log("Generating image with Gemini...", imageConfig);
|
||||||
const interaction = await ai.interactions.create({
|
const response = await postGoogleJson<{
|
||||||
model,
|
candidates?: Array<{ content?: { parts?: Array<{ inlineData?: { data?: string } }> } }>;
|
||||||
input,
|
}>(`${toModelPath(model)}:generateContent`, {
|
||||||
response_modalities: ["image"],
|
contents: [
|
||||||
generation_config: {
|
{
|
||||||
image_config: imageConfig,
|
role: "user",
|
||||||
|
parts,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
generationConfig: {
|
||||||
|
responseModalities: ["IMAGE"],
|
||||||
|
imageConfig,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
console.log("Generation completed.");
|
console.log("Generation completed.");
|
||||||
|
|
||||||
for (const output of interaction.outputs || []) {
|
const imageData = extractInlineImageData(response);
|
||||||
if (output.type === "image" && output.data) {
|
if (imageData) return Uint8Array.from(Buffer.from(imageData, "base64"));
|
||||||
return Uint8Array.from(Buffer.from(output.data, "base64"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error("No image in response");
|
throw new Error("No image in response");
|
||||||
}
|
}
|
||||||
@@ -103,30 +173,40 @@ async function generateWithImagen(
|
|||||||
model: string,
|
model: string,
|
||||||
args: CliArgs
|
args: CliArgs
|
||||||
): Promise<Uint8Array> {
|
): Promise<Uint8Array> {
|
||||||
const { experimental_generateImage: generateImage } = await import("ai");
|
|
||||||
const { createGoogleGenerativeAI } = await import("@ai-sdk/google");
|
|
||||||
|
|
||||||
const google = createGoogleGenerativeAI({
|
|
||||||
apiKey: getGoogleApiKey() || undefined,
|
|
||||||
baseURL: process.env.GOOGLE_BASE_URL,
|
|
||||||
});
|
|
||||||
|
|
||||||
const fullPrompt = buildPromptWithAspect(prompt, args.aspectRatio, args.quality);
|
const fullPrompt = buildPromptWithAspect(prompt, args.aspectRatio, args.quality);
|
||||||
|
const imageSize = getGoogleImageSize(args);
|
||||||
|
if (imageSize === "4K") {
|
||||||
|
console.error("Warning: Imagen models do not support 4K imageSize, using 2K instead.");
|
||||||
|
}
|
||||||
|
|
||||||
const result = await generateImage({
|
const parameters: Record<string, unknown> = {
|
||||||
model: google.image(model),
|
sampleCount: args.n,
|
||||||
prompt: fullPrompt,
|
};
|
||||||
n: args.n,
|
if (args.aspectRatio) {
|
||||||
aspectRatio: args.aspectRatio || undefined,
|
parameters.aspectRatio = args.aspectRatio;
|
||||||
|
}
|
||||||
|
if (imageSize === "1K" || imageSize === "2K") {
|
||||||
|
parameters.imageSize = imageSize;
|
||||||
|
} else {
|
||||||
|
parameters.imageSize = "2K";
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await postGoogleJson<{
|
||||||
|
predictions?: Array<any>;
|
||||||
|
generatedImages?: Array<any>;
|
||||||
|
}>(`${toModelPath(model)}:predict`, {
|
||||||
|
instances: [
|
||||||
|
{
|
||||||
|
prompt: fullPrompt,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
parameters,
|
||||||
});
|
});
|
||||||
|
|
||||||
const img = result.images[0];
|
const imageData = extractPredictedImageData(response);
|
||||||
if (!img) throw new Error("No image in response");
|
if (imageData) return Uint8Array.from(Buffer.from(imageData, "base64"));
|
||||||
|
|
||||||
if (img.uint8Array) return img.uint8Array;
|
throw new Error("No image in response");
|
||||||
if (img.base64) return Uint8Array.from(Buffer.from(img.base64, "base64"));
|
|
||||||
|
|
||||||
throw new Error("Cannot extract image data");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function generateImage(
|
export async function generateImage(
|
||||||
|
|||||||
Reference in New Issue
Block a user