mirror of
https://github.com/JimLiu/baoyu-skills.git
synced 2026-07-12 05:51:44 +08:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c731faea8f | |||
| fa155da15d | |||
| 6194f71378 | |||
| 8e88cf4a8b | |||
| 259baff413 | |||
| a42137ff13 |
@@ -6,7 +6,7 @@
|
||||
},
|
||||
"metadata": {
|
||||
"description": "Skills shared by Baoyu for improving daily work efficiency",
|
||||
"version": "0.5.0"
|
||||
"version": "0.6.0"
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
|
||||
@@ -149,7 +149,22 @@ Generate professional slide deck images from content. Creates comprehensive outl
|
||||
/baoyu-slide-deck path/to/article.md --lang zh
|
||||
```
|
||||
|
||||
Available styles: `editorial` (default), `corporate`, `technical`, `playful`, `minimal`, `storytelling`, `warm`, `retro-flat`, `notion`
|
||||
**Styles** (visual aesthetics):
|
||||
|
||||
| Style | Description | Best For |
|
||||
|-------|-------------|----------|
|
||||
| `notion` (default) | SaaS dashboard aesthetic with clean data focus, card-based layouts | Product demos, SaaS, productivity tools, B2B |
|
||||
| `sketch-notes` | Hand-drawn feel with soft brush strokes, warm off-white background | Educational, tutorials, knowledge sharing |
|
||||
| `blueprint` | Technical schematics with grid texture, engineering precision | Architecture, system design, data analysis |
|
||||
| `bold-editorial` | High-impact magazine style, bold typography, dark backgrounds | Product launches, marketing, keynotes |
|
||||
| `vector-illustration` | Flat vector with black outlines, retro soft colors, toy model aesthetic | Creative proposals, children's content, explainers |
|
||||
| `minimal` | Ultra-clean with maximum whitespace, single accent color, zen-like | Executive briefings, keynotes, premium brands |
|
||||
| `storytelling` | Cinematic full-bleed visuals, emotional photography | Case studies, narratives, customer journeys |
|
||||
| `warm` | Soft gradients, rounded shapes, wellness palette | Lifestyle, wellness, personal development |
|
||||
| `corporate` | Navy/gold palette, structured layouts, professional iconography | Investor decks, client proposals, quarterly reports |
|
||||
| `playful` | Vibrant coral/teal/yellow, rounded shapes, dynamic layouts | Workshops, training, creative pitches |
|
||||
|
||||
After generation, slides are automatically merged into a `.pptx` file for easy sharing.
|
||||
|
||||
### baoyu-comic
|
||||
|
||||
@@ -172,15 +187,17 @@ Knowledge comic creator supporting multiple styles (Logicomix/Ligne Claire, Ohms
|
||||
```
|
||||
|
||||
**Styles** (visual aesthetics):
|
||||
| Style | Description | Best for |
|
||||
|
||||
| Style | Description | Best For |
|
||||
|-------|-------------|----------|
|
||||
| `classic` | Traditional Ligne Claire, balanced | Biographies, balanced narratives |
|
||||
| `dramatic` | High contrast, intense | Breakthroughs, conflicts |
|
||||
| `warm` | Nostalgic, personal | Personal stories, mentorship |
|
||||
| `tech` | Modern, digital | Computing history, AI stories |
|
||||
| `sepia` | Historical, archival | Pre-1950s, classical science |
|
||||
| `vibrant` | Energetic, engaging | Science basics, young audience |
|
||||
| `ohmsha` | Manga guide style, visual metaphors | Tutorials, complex concepts (ML, physics) |
|
||||
| `classic` (default) | Traditional Ligne Claire with clean uniform outlines, flat colors, detailed backgrounds | Biographies, balanced narratives, educational content |
|
||||
| `dramatic` | High contrast with heavy shadows, intense expressions, angular compositions | Pivotal discoveries, conflicts, climactic scenes |
|
||||
| `warm` | Soft edges, golden tones, cozy interiors with nostalgic feel | Personal stories, childhood scenes, mentorship |
|
||||
| `tech` | Precise geometric lines, circuit motifs, neon accents on dark backgrounds | Computing history, AI stories, modern tech |
|
||||
| `sepia` | Vintage illustration style with aged paper effect, period-accurate details | Pre-1950s stories, classical science, historical figures |
|
||||
| `vibrant` | Energetic lines with weight variation, bright colors, dynamic poses | Science explanations, "aha" moments, young audience |
|
||||
| `ohmsha` | Manga guide style with visual metaphors, gadgets, student/mentor dynamic | Technical tutorials, complex concepts (ML, physics) |
|
||||
| `realistic` | Full-color realistic manga with digital painting, smooth gradients, accurate proportions | Wine, food, business, lifestyle, professional topics |
|
||||
|
||||
**Layouts** (panel arrangement):
|
||||
| Layout | Panels/Page | Best for |
|
||||
|
||||
+25
-8
@@ -149,7 +149,22 @@ npx add-skill jimliu/baoyu-skills
|
||||
/baoyu-slide-deck path/to/article.md --lang zh
|
||||
```
|
||||
|
||||
可用风格:`editorial`(默认)、`corporate`、`technical`、`playful`、`minimal`、`storytelling`、`warm`、`retro-flat`、`notion`
|
||||
**风格**(视觉美学):
|
||||
|
||||
| 风格 | 描述 | 适用场景 |
|
||||
|------|------|----------|
|
||||
| `notion`(默认) | SaaS 仪表盘美学,卡片式布局,数据展示清晰 | 产品演示、SaaS、生产力工具、B2B |
|
||||
| `sketch-notes` | 手绘风格,柔和笔触,暖白色背景 | 教育、教程、知识分享 |
|
||||
| `blueprint` | 技术蓝图风格,网格纹理,工程精度 | 架构设计、系统设计、数据分析 |
|
||||
| `bold-editorial` | 杂志社论风格,粗体排版,深色背景,高冲击力 | 产品发布、营销、主题演讲 |
|
||||
| `vector-illustration` | 扁平矢量风格,黑色轮廓线,复古柔和配色,玩具模型感 | 创意提案、儿童内容、说明性内容 |
|
||||
| `minimal` | 极简风格,大量留白,单一强调色,禅意美学 | 高管简报、主题演讲、高端品牌 |
|
||||
| `storytelling` | 电影风格,全幅视觉,情感化摄影 | 案例研究、叙事、客户旅程 |
|
||||
| `warm` | 柔和渐变,圆润形状,健康生活配色 | 生活方式、健康养生、个人成长 |
|
||||
| `corporate` | 海军蓝/金色配色,结构化布局,专业图标 | 投资者演示、客户提案、季度报告 |
|
||||
| `playful` | 活力珊瑚/青色/黄色,圆润形状,动感布局 | 工作坊、培训、创意提案 |
|
||||
|
||||
生成完成后,所有幻灯片会自动合并为 `.pptx` 文件,方便分享。
|
||||
|
||||
### baoyu-comic
|
||||
|
||||
@@ -172,15 +187,17 @@ npx add-skill jimliu/baoyu-skills
|
||||
```
|
||||
|
||||
**风格**(视觉美学):
|
||||
|
||||
| 风格 | 描述 | 适用场景 |
|
||||
|------|------|----------|
|
||||
| `classic` | 传统清线风格,平衡 | 传记、平衡叙事 |
|
||||
| `dramatic` | 高对比度,紧张感 | 突破时刻、冲突 |
|
||||
| `warm` | 怀旧、个人化 | 个人故事、师生情 |
|
||||
| `tech` | 现代、数字化 | 计算机史、AI 故事 |
|
||||
| `sepia` | 历史、档案感 | 1950 年前、古典科学 |
|
||||
| `vibrant` | 活力、吸引人 | 科普、青少年读者 |
|
||||
| `ohmsha` | 欧姆社漫画风格,视觉比喻 | 教程、复杂概念(机器学习、物理) |
|
||||
| `classic`(默认) | 传统清线风格,统一线条、平涂色彩、精细背景 | 传记、平衡叙事、教育内容 |
|
||||
| `dramatic` | 高对比度,重阴影、紧张表情、棱角分明的构图 | 重大发现、冲突、高潮场景 |
|
||||
| `warm` | 柔和边缘、金色调、温馨室内、怀旧感 | 个人故事、童年场景、师生情 |
|
||||
| `tech` | 精确几何线条、电路纹理、深色背景配霓虹色 | 计算机史、AI 故事、现代科技 |
|
||||
| `sepia` | 复古插画风格、做旧纸张效果、时代准确细节 | 1950 年前故事、古典科学、历史人物 |
|
||||
| `vibrant` | 富有活力的线条、明亮色彩、动感姿态 | 科学解说、"顿悟"时刻、青少年读者 |
|
||||
| `ohmsha` | 欧姆社漫画风格,视觉比喻、道具、学生/导师互动 | 技术教程、复杂概念(机器学习、物理) |
|
||||
| `realistic` | 全彩写实日漫风格,数字绘画、平滑渐变、准确人体比例 | 红酒、美食、商业、生活方式、专业话题 |
|
||||
|
||||
**布局**(分镜排列):
|
||||
| 布局 | 每页分镜数 | 适用场景 |
|
||||
|
||||
+92
-335
@@ -5,384 +5,141 @@ description: Knowledge comic creator supporting multiple styles (Logicomix/Ligne
|
||||
|
||||
# Knowledge Comic Creator
|
||||
|
||||
Create original knowledge comics supporting multiple visual styles: Logicomix-style Ligne Claire, Ohmsha manga guide, and more.
|
||||
Create original knowledge comics with multiple visual styles.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
# From source material
|
||||
/baoyu-comic posts/turing-story/source.md
|
||||
|
||||
# Specify style
|
||||
/baoyu-comic posts/turing-story/source.md --style dramatic
|
||||
|
||||
# Specify layout preference
|
||||
/baoyu-comic posts/turing-story/source.md --layout cinematic
|
||||
|
||||
# Direct content input
|
||||
/baoyu-comic
|
||||
[paste story content or topic description]
|
||||
|
||||
# Direct input with options
|
||||
/baoyu-comic --style warm --layout dense
|
||||
[paste content]
|
||||
/baoyu-comic posts/turing-story/source.md --style dramatic --layout cinematic
|
||||
/baoyu-comic # then paste content
|
||||
```
|
||||
|
||||
## Options
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `--style <name>` | Visual style (see Style Gallery) |
|
||||
| `--layout <name>` | Panel layout preference (see Layout Gallery) |
|
||||
| Option | Values |
|
||||
|--------|--------|
|
||||
| `--style` | classic (default), dramatic, warm, tech, sepia, vibrant, ohmsha, realistic |
|
||||
| `--layout` | standard (default), cinematic, dense, splash, mixed, webtoon |
|
||||
|
||||
## Two Dimensions
|
||||
Style × Layout can be freely combined.
|
||||
|
||||
| Dimension | Controls | Options |
|
||||
|-----------|----------|---------|
|
||||
| **Style** | Color palette, mood, era treatment | classic, dramatic, warm, tech, sepia, vibrant, ohmsha |
|
||||
| **Layout** | Panel arrangement, density | standard, cinematic, dense, splash, mixed, webtoon |
|
||||
## Auto Selection
|
||||
|
||||
Style × Layout can be freely combined. Example: `--style dramatic --layout cinematic` creates high-contrast panels with wide aspect ratios.
|
||||
| Content Signals | Style | Layout |
|
||||
|-----------------|-------|--------|
|
||||
| Tutorial, how-to, beginner | ohmsha | webtoon |
|
||||
| Computing, AI, programming | tech | dense |
|
||||
| Pre-1950, classical, ancient | sepia | cinematic |
|
||||
| Personal story, mentor | warm | standard |
|
||||
| Conflict, breakthrough | dramatic | splash |
|
||||
| Wine, food, business, lifestyle, professional | realistic | cinematic |
|
||||
| Biography, balanced | classic | mixed |
|
||||
|
||||
## Style Gallery
|
||||
|
||||
| Style | Description |
|
||||
|-------|-------------|
|
||||
| `classic` (Default) | Traditional Ligne Claire, balanced and timeless |
|
||||
| `dramatic` | High contrast, intense moments |
|
||||
| `warm` | Nostalgic, personal storytelling |
|
||||
| `tech` | Modern, digital-age narratives |
|
||||
| `sepia` | Historical, archival feel |
|
||||
| `vibrant` | Energetic, engaging, educational |
|
||||
| `ohmsha` | Ohmsha Manga Guide style - educational manga with visual metaphors |
|
||||
|
||||
Detailed style definitions: `references/styles/<style>.md`
|
||||
|
||||
## Layout Gallery
|
||||
|
||||
| Layout | Description |
|
||||
|--------|-------------|
|
||||
| `standard` (Default) | Classic comic grid, versatile (4-6 panels) |
|
||||
| `cinematic` | Wide panels, filmic feel (2-4 panels) |
|
||||
| `dense` | Information-rich, educational focus (6-9 panels) |
|
||||
| `splash` | Impact-focused, key moments (1-2 large + 2-3 small) |
|
||||
| `mixed` | Dynamic, varied rhythm (3-7 panels) |
|
||||
| `webtoon` | Vertical scrolling comic (竖版条漫) |
|
||||
|
||||
Detailed layout definitions: `references/layouts/<layout>.md`
|
||||
|
||||
## Auto Style Selection
|
||||
|
||||
When no `--style` is specified, analyze content to select:
|
||||
|
||||
| Content Signals | Selected Style |
|
||||
|----------------|----------------|
|
||||
| Tutorial, how-to, learning guide, beginner | `ohmsha` |
|
||||
| Computing, AI, digital, programming | `tech` |
|
||||
| Pre-1950, classical, ancient | `sepia` |
|
||||
| Personal story, childhood, mentor | `warm` |
|
||||
| Conflict, struggle, breakthrough | `dramatic` |
|
||||
| Young audience, science basics, wonder | `vibrant` |
|
||||
| Balanced narrative, biography | `classic` |
|
||||
|
||||
## Auto Layout Selection
|
||||
|
||||
When no `--layout` is specified, analyze content to select:
|
||||
|
||||
| Content Signals | Selected Layout |
|
||||
|----------------|-----------------|
|
||||
| Technical explanation, timeline | `dense` |
|
||||
| Key discovery, major event | `splash` |
|
||||
| Establishing scene, location | `cinematic` |
|
||||
| Dialogue, conversation, debate | `standard` |
|
||||
| Mixed content, varied pacing | `mixed` |
|
||||
| Ohmsha style, tutorial, vertical scroll | `webtoon` |
|
||||
|
||||
## Style × Layout Matrix
|
||||
|
||||
| | standard | cinematic | dense | splash | mixed | webtoon |
|
||||
|---|:---:|:---:|:---:|:---:|:---:|:---:|
|
||||
| classic | ✓✓ | ✓✓ | ✓ | ✓✓ | ✓✓ | ✓ |
|
||||
| dramatic | ✓ | ✓✓ | ✓ | ✓✓ | ✓✓ | ✓ |
|
||||
| warm | ✓✓ | ✓✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
| tech | ✓✓ | ✓ | ✓✓ | ✓ | ✓✓ | ✓✓ |
|
||||
| sepia | ✓✓ | ✓✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
| vibrant | ✓✓ | ✓ | ✓ | ✓✓ | ✓✓ | ✓✓ |
|
||||
| ohmsha | ✓ | ✓ | ✓✓ | ✓ | ✓✓ | ✓✓ |
|
||||
|
||||
✓✓ = highly recommended, ✓ = works well
|
||||
|
||||
## File Management
|
||||
|
||||
### With Source Path
|
||||
|
||||
Save to `comic/` subdirectory in the same folder as the source:
|
||||
## File Structure
|
||||
|
||||
```
|
||||
posts/turing-story/
|
||||
├── source.md
|
||||
└── comic/
|
||||
├── outline.md
|
||||
├── prompts/
|
||||
│ ├── 00-cover.md
|
||||
│ ├── 01-page.md
|
||||
│ └── ...
|
||||
├── 00-cover.png
|
||||
├── 01-page.png
|
||||
└── ...
|
||||
[target]/
|
||||
├── outline.md
|
||||
├── characters/
|
||||
│ ├── characters.md # Character definitions
|
||||
│ └── characters.png # Character reference sheet
|
||||
├── prompts/
|
||||
│ ├── 00-cover.md
|
||||
│ └── XX-page.md
|
||||
├── 00-cover.png
|
||||
└── XX-page.png
|
||||
```
|
||||
|
||||
### Without Source Path
|
||||
|
||||
Save to `comic-outputs/YYYY-MM-DD/[topic-slug]/`:
|
||||
|
||||
```
|
||||
comic-outputs/
|
||||
└── 2026-01-16/
|
||||
└── turing-biography/
|
||||
├── outline.md
|
||||
├── prompts/
|
||||
│ ├── 00-cover.md
|
||||
│ └── ...
|
||||
├── 00-cover.png
|
||||
└── ...
|
||||
```
|
||||
|
||||
**Directory collision**: If slug exists, generate a different slug (do not overwrite).
|
||||
**Target directory**:
|
||||
- With source path: `[source-dir]/comic/`
|
||||
- Without source: `comic-outputs/YYYY-MM-DD/[topic-slug]/`
|
||||
|
||||
## Workflow
|
||||
|
||||
### Step 1: Analyze Content & Determine Scope
|
||||
### Step 1: Analyze Content
|
||||
|
||||
1. Read source content
|
||||
2. If `--style` specified, use it; otherwise auto-select
|
||||
3. If `--layout` specified, use it; otherwise auto-select per page
|
||||
4. Determine page count based on content complexity:
|
||||
2. Select style (from `--style` or auto-detect)
|
||||
3. Select layout (from `--layout` or auto-detect per page)
|
||||
4. Determine page count:
|
||||
- Short story: 5-8 pages
|
||||
- Medium complexity: 9-15 pages
|
||||
- Full biography: 16-25 pages
|
||||
|
||||
| Content Type | Page Count (incl. cover) |
|
||||
|-------------|--------------------------|
|
||||
| Single event / short story | 5-8 |
|
||||
| Medium complexity / mini biography | 9-15 |
|
||||
| Full biography / multi-thread | 16-25 |
|
||||
### Step 2: Define Characters
|
||||
|
||||
### Step 2: Generate Outline
|
||||
**Purpose**: Establish visual consistency across all pages.
|
||||
|
||||
Plan cover + each page with detailed panel breakdowns:
|
||||
1. Extract all characters from content (protagonist, supporting, antagonist, narrator)
|
||||
2. Create `characters/characters.md` with visual specs for each character
|
||||
3. Generate `characters/characters.png` (character reference sheet)
|
||||
|
||||
```markdown
|
||||
# [Comic Title] - Knowledge Biography Comic Outline
|
||||
**Reference**: `references/character-template.md` for detailed format and examples.
|
||||
|
||||
**Topic**: [topic description]
|
||||
**Time Span**: [e.g., 1912-1954]
|
||||
**Style**: [selected style]
|
||||
**Default Layout**: [selected layout or "varies"]
|
||||
**Page Count**: Cover + N pages
|
||||
**Generated**: YYYY-MM-DD HH:mm
|
||||
### Step 3: Generate Outline
|
||||
|
||||
---
|
||||
Create `outline.md` with:
|
||||
- Metadata (title, style, layout, page count, character reference path)
|
||||
- Cover design
|
||||
- Each page: layout, panel breakdown, visual prompts
|
||||
|
||||
## Cover
|
||||
**Reference**: `references/outline-template.md` for detailed format.
|
||||
|
||||
**Filename**: 00-cover.png
|
||||
**Core Message**: [one-liner]
|
||||
### Step 4: Generate Images
|
||||
|
||||
**Visual Design**:
|
||||
- Title typography style
|
||||
- Main visual composition
|
||||
- Color scheme
|
||||
- Subtitle / time span notation
|
||||
For each page (cover + pages):
|
||||
|
||||
**Visual Prompt**:
|
||||
[Detailed image generation prompt]
|
||||
1. Save prompt to `prompts/XX-page.md`
|
||||
2. Call image generation skill with:
|
||||
- Base prompt: `references/base-prompt.md`
|
||||
- Character reference (text or image, depending on skill capability)
|
||||
- Page prompt
|
||||
- Output path
|
||||
|
||||
---
|
||||
**Image Generation Skill Selection**:
|
||||
- Check available image generation skills in the environment
|
||||
- Adapt parameters based on skill capabilities:
|
||||
- If supports `--promptfiles`: pass prompt files
|
||||
- If supports reference image: pass `characters/characters.png`
|
||||
- If text-only: concatenate prompts into single text
|
||||
- If multiple skills available, ask user preference
|
||||
|
||||
## Page 1 / N
|
||||
**Session Management**:
|
||||
If the image generation skill supports `--sessionId`:
|
||||
1. Generate a unique session ID at the start (e.g., `comic-{topic-slug}-{timestamp}`)
|
||||
2. Use the same session ID for character sheet and all pages
|
||||
3. This ensures visual consistency (character appearance, style) across all generated images
|
||||
|
||||
**Filename**: 01-page.png
|
||||
**Layout**: [standard/cinematic/dense/splash/mixed]
|
||||
**Narrative Layer**: [Main narrative / Narrator layer / Mixed]
|
||||
**Core Message**: [What this page conveys]
|
||||
|
||||
### Panel Layout
|
||||
|
||||
**Panel Count**: X
|
||||
**Layout Type**: [grid/irregular/splash]
|
||||
|
||||
#### Panel 1 (Size: 1/3 page, Position: Top)
|
||||
|
||||
**Scene**: [Time, location]
|
||||
**Image Description**:
|
||||
- Camera angle: [bird's eye / low angle / eye level / close-up / wide shot]
|
||||
- Characters: [pose, expression, action]
|
||||
- Environment: [scene details, period markers]
|
||||
- Lighting: [atmosphere description]
|
||||
- Color tone: [palette reference]
|
||||
|
||||
**Text Elements**:
|
||||
- Dialogue bubble (oval): "Character line"
|
||||
- Narrator box (rectangular): 「Narrator commentary」
|
||||
- Caption bar: [Background info text]
|
||||
|
||||
#### Panel 2...
|
||||
|
||||
**Page Hook**: [Cliffhanger or transition at page end]
|
||||
|
||||
**Visual Prompt**:
|
||||
[Full page image generation prompt]
|
||||
|
||||
---
|
||||
|
||||
## Page 2 / N
|
||||
...
|
||||
```
|
||||
|
||||
### Step 3: Save Outline
|
||||
|
||||
Save as `outline.md` in target directory.
|
||||
|
||||
### Step 4: Generate Images Page by Page
|
||||
|
||||
For each page:
|
||||
|
||||
1. **Save prompt file** to `prompts/` subdirectory
|
||||
2. **Call image generation skill**:
|
||||
|
||||
```bash
|
||||
npx -y bun skills/baoyu-gemini-web/scripts/main.ts \
|
||||
--promptfiles skills/baoyu-comic/prompts/system.md [target]/prompts/00-cover.md \
|
||||
--image [target]/00-cover.png
|
||||
```
|
||||
|
||||
After each generation:
|
||||
1. Confirm success
|
||||
2. Report progress: "Generated X/N pages"
|
||||
3. Continue to next
|
||||
3. Report progress after each generation
|
||||
|
||||
### Step 5: Completion Report
|
||||
|
||||
```
|
||||
Knowledge Biography Comic Complete!
|
||||
|
||||
Title: [Comic Title]
|
||||
Time Span: [e.g., 1912-1954]
|
||||
Style: [style name]
|
||||
Layout: [layout name or "varies"]
|
||||
Location: [directory path]
|
||||
Pages: Cover + N pages
|
||||
|
||||
- 00-cover.png ✓ Cover
|
||||
- 01-page.png ✓ Opening: [brief]
|
||||
- 02-page.png ✓ [brief]
|
||||
- ...
|
||||
- XX-page.png ✓ Ending: [brief]
|
||||
|
||||
Outline: outline.md
|
||||
Comic Complete!
|
||||
Title: [title] | Style: [style] | Pages: [count]
|
||||
Location: [path]
|
||||
✓ characters.png
|
||||
✓ 00-cover.png ... XX-page.png
|
||||
```
|
||||
|
||||
## Narrative Structure Principles
|
||||
## Style-Specific Guidelines
|
||||
|
||||
### Cover Design
|
||||
### Ohmsha Style (`--style ohmsha`)
|
||||
|
||||
- Academic gravitas with visual appeal
|
||||
- Title typography reflecting knowledge/science theme
|
||||
- Composition hinting at core theme (character silhouette, iconic symbol, concept diagram)
|
||||
- Subtitle or time span for epic scope
|
||||
Additional requirements for educational manga:
|
||||
- Default characters: Student (大雄), Mentor (哆啦A梦), Antagonist (胖虎)
|
||||
- Custom: `--characters "Student:小明,Mentor:教授"`
|
||||
- Must use visual metaphors (gadgets, action scenes) - NO talking heads
|
||||
- Page titles: narrative style, not "Page X: Topic"
|
||||
|
||||
### Panel Composition Principles
|
||||
**Reference**: `references/ohmsha-guide.md` for detailed guidelines.
|
||||
|
||||
| Panel Type | Recommended Count | Usage |
|
||||
|-----------|-------------------|-------|
|
||||
| Main narrative | 3-5 per page | Story progression |
|
||||
| Concept diagram | 1-2 per page | Visualize abstractions |
|
||||
| Narrator panel | 0-1 per page | Commentary, transition, questions |
|
||||
| Splash (full/half) | Occasional | Major moments |
|
||||
## References
|
||||
|
||||
### Panel Size Reference
|
||||
|
||||
- **Full page (Splash)**: Major moments, key breakthroughs
|
||||
- **Half page**: Important scenes, turning points
|
||||
- **1/3 page**: Standard narrative panels
|
||||
- **1/4 or smaller**: Quick progression, sequential action
|
||||
|
||||
### Concept Visualization Techniques
|
||||
|
||||
Transform abstract concepts into concrete visuals:
|
||||
|
||||
| Abstract Concept | Visual Approach |
|
||||
|-----------------|-----------------|
|
||||
| Neural network | Glowing nodes with connecting lines |
|
||||
| Gradient descent | Ball rolling down valley terrain |
|
||||
| Data flow | Luminous particles flowing through pipes |
|
||||
| Algorithm iteration | Ascending spiral staircase |
|
||||
| Breakthrough moment | Shattering barrier, piercing light |
|
||||
| Logical proof | Building blocks assembling |
|
||||
| Uncertainty | Forking paths, fog, multiple shadows |
|
||||
|
||||
### Text Element Design
|
||||
|
||||
| Text Type | Style | Usage |
|
||||
|-----------|-------|-------|
|
||||
| Character dialogue | Oval speech bubble | Main narrative speech |
|
||||
| Narrator commentary | Rectangular / hand-drawn box | Explanation, commentary |
|
||||
| Caption bar | Edge-mounted rectangle | Time, location info |
|
||||
| Thought bubble | Cloud shape | Character inner monologue |
|
||||
| Term label | Bold / special color | First appearance of technical terms |
|
||||
|
||||
## Ohmsha Style Special Requirements
|
||||
|
||||
When using `--style ohmsha`, follow these additional guidelines:
|
||||
|
||||
### Character Setup (Required)
|
||||
|
||||
Define characters before generating outline:
|
||||
|
||||
| Role | Default | Traits |
|
||||
|------|---------|--------|
|
||||
| Student (Role A) | 大雄 | Confused, asks basic but crucial questions, represents reader |
|
||||
| Mentor (Role B) | 哆啦A梦 | Knowledgeable, patient, uses gadgets as technical metaphors |
|
||||
| Antagonist (Role C, optional) | 胖虎 | Represents misunderstanding, or "noise" in the data |
|
||||
|
||||
User can specify custom characters via `--characters "Student:小明,Mentor:教授,Antagonist:Bug怪"`
|
||||
|
||||
### Outline Spec Block (Required)
|
||||
|
||||
Every ohmsha outline must start with a spec block:
|
||||
|
||||
```markdown
|
||||
【漫画规格单】
|
||||
- Language: [Same as input content]
|
||||
- Style: Ohmsha (Manga Guide), Full Color
|
||||
- Layout: Vertical Scrolling Comic (竖版条漫)
|
||||
- Characters: [List character names and roles]
|
||||
- Page Limit: ≤20 pages
|
||||
```
|
||||
|
||||
### Visual Metaphor Rules (Critical)
|
||||
|
||||
**NEVER** create "talking heads" panels. Every technical concept must become:
|
||||
|
||||
1. **A tangible gadget/prop** - Something characters can hold, use, demonstrate
|
||||
2. **An action scene** - Characters doing something that illustrates the concept
|
||||
3. **A visual environment** - Stepping into a metaphorical space
|
||||
|
||||
### Page Title Convention
|
||||
|
||||
Avoid AI-style "Title: Subtitle" format. Use narrative descriptions:
|
||||
- ❌ "Page 3: Introduction to Neural Networks"
|
||||
- ✓ "Page 3: 大雄被海量单词淹没,哆啦A梦拿出'词向量压缩机'"
|
||||
|
||||
### Ending Requirements
|
||||
|
||||
- NO generic endings ("What will you choose?", "Thanks for reading")
|
||||
- End with: Technical summary moment OR character achieving a small goal
|
||||
- Final panel: Sense of accomplishment, not open-ended question
|
||||
|
||||
## Notes
|
||||
|
||||
- Image generation typically takes 30-60 seconds per page
|
||||
- Auto-retry once on generation failure
|
||||
- Historical figures rendered in portrait/illustration style for recognizability
|
||||
- All text in Chinese unless source is in another language
|
||||
- Each panel description should be detailed enough to serve as standalone AI art prompt
|
||||
- Maintain style consistency across all pages in series
|
||||
Detailed templates and guidelines in `references/` directory:
|
||||
- `character-template.md` - Character definition format and examples
|
||||
- `outline-template.md` - Outline structure and panel breakdown
|
||||
- `ohmsha-guide.md` - Ohmsha manga style specifics
|
||||
- `styles/` - Detailed style definitions
|
||||
- `layouts/` - Detailed layout definitions
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
# Character Definition Template
|
||||
|
||||
## Character Document Format
|
||||
|
||||
Create `characters/characters.md` with the following structure:
|
||||
|
||||
```markdown
|
||||
# Character Definitions - [Comic Title]
|
||||
|
||||
**Style**: [selected style]
|
||||
**Art Direction**: [Ligne Claire / Manga / etc.]
|
||||
|
||||
---
|
||||
|
||||
## Character 1: [Name]
|
||||
|
||||
**Role**: [Protagonist / Mentor / Antagonist / Narrator]
|
||||
**Age**: [approximate age or age range in story]
|
||||
|
||||
**Appearance**:
|
||||
- Face shape: [oval/square/round]
|
||||
- Hair: [color, style, length]
|
||||
- Eyes: [color, shape, distinctive features]
|
||||
- Build: [height, body type]
|
||||
- Distinguishing features: [glasses, beard, scar, etc.]
|
||||
|
||||
**Costume**:
|
||||
- Default outfit: [detailed description]
|
||||
- Color palette: [primary colors for this character]
|
||||
- Accessories: [hat, bag, tools, etc.]
|
||||
|
||||
**Expression Range**:
|
||||
- Neutral: [description]
|
||||
- Happy/Excited: [description]
|
||||
- Thinking/Confused: [description]
|
||||
- Determined: [description]
|
||||
|
||||
**Visual Reference Notes**:
|
||||
[Any specific artistic direction]
|
||||
|
||||
---
|
||||
|
||||
## Character 2: [Name]
|
||||
...
|
||||
```
|
||||
|
||||
## Reference Sheet Image Prompt
|
||||
|
||||
After character definitions, include a prompt for generating the reference sheet:
|
||||
|
||||
```markdown
|
||||
## Reference Sheet Prompt
|
||||
|
||||
Character reference sheet in [style] style, clean lines, flat colors:
|
||||
|
||||
[ROW 1 - Character Name]:
|
||||
- Front view: [detailed description]
|
||||
- 3/4 view: [description]
|
||||
- Expression sheet: Neutral | Happy | Focused | Worried
|
||||
|
||||
[ROW 2 - Character Name]:
|
||||
...
|
||||
|
||||
COLOR PALETTE:
|
||||
- [Character 1]: [colors]
|
||||
- [Character 2]: [colors]
|
||||
|
||||
White background, clear labels under each character.
|
||||
```
|
||||
|
||||
## Example: Turing Biography
|
||||
|
||||
```markdown
|
||||
# Character Definitions - The Imitation Game
|
||||
|
||||
**Style**: classic (Ligne Claire)
|
||||
**Art Direction**: Clean lines, muted colors, period-accurate details
|
||||
|
||||
---
|
||||
|
||||
## Character 1: Alan Turing
|
||||
|
||||
**Role**: Protagonist
|
||||
**Age**: 25-40 (varies across story)
|
||||
|
||||
**Appearance**:
|
||||
- Face shape: Oval, slightly angular
|
||||
- Hair: Dark brown, wavy, slightly disheveled
|
||||
- Eyes: Deep-set, intense gaze
|
||||
- Build: Tall, lean, slightly awkward posture
|
||||
- Distinguishing features: Prominent brow, thoughtful expression
|
||||
|
||||
**Costume**:
|
||||
- Default outfit: Tweed jacket with elbow patches, white shirt, no tie
|
||||
- Color palette: Muted browns, navy blue, cream
|
||||
- Accessories: Occasionally a pipe, papers/notebooks
|
||||
|
||||
**Expression Range**:
|
||||
- Neutral: Thoughtful, slightly distant
|
||||
- Happy/Excited: Eureka moment, eyes bright, subtle smile
|
||||
- Thinking/Confused: Furrowed brow, looking at abstract space
|
||||
- Determined: Jaw set, focused eyes
|
||||
|
||||
---
|
||||
|
||||
## Character 2: The Bombe Machine
|
||||
|
||||
**Role**: Supporting (anthropomorphized)
|
||||
**Appearance**:
|
||||
- Large brass and wood cabinet
|
||||
- Dial "eyes" that can express states
|
||||
- Paper tape "mouth"
|
||||
- Indicator lights for emotions
|
||||
|
||||
**Expression Range**:
|
||||
- Processing: Spinning dials, humming
|
||||
- Success: Lights up warmly
|
||||
- Stuck: Smoke wisps, stuttering
|
||||
|
||||
---
|
||||
|
||||
## Reference Sheet Prompt
|
||||
|
||||
Character reference sheet in Ligne Claire style, clean lines, flat colors:
|
||||
|
||||
TOP ROW - Alan Turing:
|
||||
- Front view: Young man, 30s, short dark wavy hair, thoughtful expression, wearing tweed jacket with elbow patches, white shirt
|
||||
- 3/4 view: Same character, slight smile, showing profile of nose
|
||||
- Expression sheet: Neutral | Excited (eureka moment) | Focused (working) | Worried
|
||||
|
||||
BOTTOM ROW - The Bombe Machine (anthropomorphized):
|
||||
- Bombe machine as character: Large, brass and wood, dial "eyes", paper tape "mouth"
|
||||
- Expressions: Processing (spinning dials) | Success (lights up) | Stuck (smoke wisps)
|
||||
|
||||
COLOR PALETTE:
|
||||
- Turing: Muted browns (#8B7355), navy blue (#2C3E50), cream (#F5F5DC)
|
||||
- Machine: Brass (#B5A642), mahogany (#4E2728), emerald indicators (#2ECC71)
|
||||
|
||||
White background, clear labels under each character.
|
||||
```
|
||||
|
||||
## Handling Age Variants
|
||||
|
||||
For biographies spanning many years, define age variants:
|
||||
|
||||
```markdown
|
||||
## Alan Turing - Age Variants
|
||||
|
||||
### Young (1920s, age 10-18)
|
||||
- Boyish features, round face
|
||||
- School uniform (Sherborne)
|
||||
- Curious, eager expression
|
||||
|
||||
### Adult (1930s-40s, age 25-35)
|
||||
- Angular face, defined jaw
|
||||
- Tweed jacket, rumpled appearance
|
||||
- Intense, focused expression
|
||||
|
||||
### Later (1950s, age 40+)
|
||||
- Slightly weathered
|
||||
- More casual dress
|
||||
- Thoughtful, sometimes melancholic
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
| Practice | Description |
|
||||
|----------|-------------|
|
||||
| Be specific | "Short dark wavy hair, parted left" not just "dark hair" |
|
||||
| Use distinguishing features | Glasses, scars, accessories that identify character |
|
||||
| Define color codes | Use specific color names or hex codes |
|
||||
| Include age markers | Wrinkles, posture, clothing style matching era |
|
||||
| Reference real people | For historical figures, note "based on 1940s photographs" |
|
||||
|
||||
## Why Character Reference Matters
|
||||
|
||||
Without unified character definition, AI generates inconsistent appearances. The reference sheet provides:
|
||||
1. Visual anchors for consistent features
|
||||
2. Color palettes for consistent coloring
|
||||
3. Expression documentation for emotional portrayals
|
||||
@@ -0,0 +1,85 @@
|
||||
# Ohmsha Manga Guide Style
|
||||
|
||||
Guidelines for `--style ohmsha` educational manga comics.
|
||||
|
||||
## Character Setup
|
||||
|
||||
| Role | Default | Traits |
|
||||
|------|---------|--------|
|
||||
| Student (Role A) | 大雄 | Confused, asks basic but crucial questions, represents reader |
|
||||
| Mentor (Role B) | 哆啦A梦 | Knowledgeable, patient, uses gadgets as technical metaphors |
|
||||
| Antagonist (Role C, optional) | 胖虎 | Represents misunderstanding, or "noise" in the data |
|
||||
|
||||
Custom characters: `--characters "Student:小明,Mentor:教授,Antagonist:Bug怪"`
|
||||
|
||||
## Character Reference Sheet Style
|
||||
|
||||
For Ohmsha style, use manga/anime style with:
|
||||
- Exaggerated expressions for educational clarity
|
||||
- Simple, distinctive silhouettes
|
||||
- Bright, saturated color palettes
|
||||
- Chibi/SD (super-deformed) variants for comedic reactions
|
||||
|
||||
## Outline Spec Block
|
||||
|
||||
Every ohmsha outline must start with:
|
||||
|
||||
```markdown
|
||||
【漫画规格单】
|
||||
- Language: [Same as input content]
|
||||
- Style: Ohmsha (Manga Guide), Full Color
|
||||
- Layout: Vertical Scrolling Comic (竖版条漫)
|
||||
- Characters: [List character names and roles]
|
||||
- Character Reference: characters/characters.png
|
||||
- Page Limit: ≤20 pages
|
||||
```
|
||||
|
||||
## Visual Metaphor Rules (Critical)
|
||||
|
||||
**NEVER** create "talking heads" panels. Every technical concept must become:
|
||||
|
||||
1. **A tangible gadget/prop** - Something characters can hold, use, demonstrate
|
||||
2. **An action scene** - Characters doing something that illustrates the concept
|
||||
3. **A visual environment** - Stepping into a metaphorical space
|
||||
|
||||
### Examples
|
||||
|
||||
| Concept | Bad (Talking Heads) | Good (Visual Metaphor) |
|
||||
|---------|---------------------|------------------------|
|
||||
| Word embeddings | Characters discussing vectors | 哆啦A梦拿出"词向量压缩机",把书本压缩成彩色小球 |
|
||||
| Gradient descent | Explaining math formula | 大雄在山谷地形上滚球,寻找最低点 |
|
||||
| Neural network | Diagram on whiteboard | 角色走进由发光节点组成的网络迷宫 |
|
||||
|
||||
## Page Title Convention
|
||||
|
||||
Avoid AI-style "Title: Subtitle" format. Use narrative descriptions:
|
||||
|
||||
- ❌ "Page 3: Introduction to Neural Networks"
|
||||
- ✓ "Page 3: 大雄被海量单词淹没,哆啦A梦拿出'词向量压缩机'"
|
||||
|
||||
## Ending Requirements
|
||||
|
||||
- NO generic endings ("What will you choose?", "Thanks for reading")
|
||||
- End with: Technical summary moment OR character achieving a small goal
|
||||
- Final panel: Sense of accomplishment, not open-ended question
|
||||
|
||||
### Good Endings
|
||||
|
||||
- Student successfully applies learned concept
|
||||
- Visual callback to opening problem, now solved
|
||||
- Mentor gives summary while student demonstrates understanding
|
||||
|
||||
### Bad Endings
|
||||
|
||||
- "What do you think?" open questions
|
||||
- "Thanks for reading this tutorial"
|
||||
- Cliffhanger without resolution
|
||||
|
||||
## Layout Preference
|
||||
|
||||
Ohmsha style typically uses:
|
||||
- `webtoon` (vertical scrolling) - Primary choice
|
||||
- `dense` - For information-heavy sections
|
||||
- `mixed` - For varied pacing
|
||||
|
||||
Avoid `cinematic` and `splash` for educational content.
|
||||
@@ -0,0 +1,136 @@
|
||||
# Outline Template
|
||||
|
||||
## Outline Document Format
|
||||
|
||||
```markdown
|
||||
# [Comic Title] - Knowledge Comic Outline
|
||||
|
||||
**Topic**: [topic description]
|
||||
**Time Span**: [e.g., 1912-1954]
|
||||
**Style**: [selected style]
|
||||
**Default Layout**: [selected layout or "varies"]
|
||||
**Page Count**: Cover + N pages
|
||||
**Character Reference**: characters/characters.png
|
||||
**Generated**: YYYY-MM-DD HH:mm
|
||||
|
||||
---
|
||||
|
||||
## Cover
|
||||
|
||||
**Filename**: 00-cover.png
|
||||
**Core Message**: [one-liner]
|
||||
|
||||
**Visual Design**:
|
||||
- Title typography style
|
||||
- Main visual composition
|
||||
- Color scheme
|
||||
- Subtitle / time span notation
|
||||
|
||||
**Visual Prompt**:
|
||||
[Detailed image generation prompt]
|
||||
|
||||
---
|
||||
|
||||
## Page 1 / N
|
||||
|
||||
**Filename**: 01-page.png
|
||||
**Layout**: [standard/cinematic/dense/splash/mixed]
|
||||
**Narrative Layer**: [Main narrative / Narrator layer / Mixed]
|
||||
**Core Message**: [What this page conveys]
|
||||
|
||||
### Panel Layout
|
||||
|
||||
**Panel Count**: X
|
||||
**Layout Type**: [grid/irregular/splash]
|
||||
|
||||
#### Panel 1 (Size: 1/3 page, Position: Top)
|
||||
|
||||
**Scene**: [Time, location]
|
||||
**Image Description**:
|
||||
- Camera angle: [bird's eye / low angle / eye level / close-up / wide shot]
|
||||
- Characters: [pose, expression, action]
|
||||
- Environment: [scene details, period markers]
|
||||
- Lighting: [atmosphere description]
|
||||
- Color tone: [palette reference]
|
||||
|
||||
**Text Elements**:
|
||||
- Dialogue bubble (oval): "Character line"
|
||||
- Narrator box (rectangular): 「Narrator commentary」
|
||||
- Caption bar: [Background info text]
|
||||
|
||||
#### Panel 2...
|
||||
|
||||
**Page Hook**: [Cliffhanger or transition at page end]
|
||||
|
||||
**Visual Prompt**:
|
||||
[Full page image generation prompt]
|
||||
|
||||
---
|
||||
|
||||
## Page 2 / N
|
||||
...
|
||||
```
|
||||
|
||||
## Cover Design Principles
|
||||
|
||||
- Academic gravitas with visual appeal
|
||||
- Title typography reflecting knowledge/science theme
|
||||
- Composition hinting at core theme (character silhouette, iconic symbol, concept diagram)
|
||||
- Subtitle or time span for epic scope
|
||||
|
||||
## Panel Composition Guidelines
|
||||
|
||||
| Panel Type | Recommended Count | Usage |
|
||||
|-----------|-------------------|-------|
|
||||
| Main narrative | 3-5 per page | Story progression |
|
||||
| Concept diagram | 1-2 per page | Visualize abstractions |
|
||||
| Narrator panel | 0-1 per page | Commentary, transition |
|
||||
| Splash (full/half) | Occasional | Major moments |
|
||||
|
||||
## Panel Size Reference
|
||||
|
||||
- **Full page (Splash)**: Major moments, key breakthroughs
|
||||
- **Half page**: Important scenes, turning points
|
||||
- **1/3 page**: Standard narrative panels
|
||||
- **1/4 or smaller**: Quick progression, sequential action
|
||||
|
||||
## Concept Visualization Techniques
|
||||
|
||||
Transform abstract concepts into concrete visuals:
|
||||
|
||||
| Abstract Concept | Visual Approach |
|
||||
|-----------------|-----------------|
|
||||
| Neural network | Glowing nodes with connecting lines |
|
||||
| Gradient descent | Ball rolling down valley terrain |
|
||||
| Data flow | Luminous particles flowing through pipes |
|
||||
| Algorithm iteration | Ascending spiral staircase |
|
||||
| Breakthrough moment | Shattering barrier, piercing light |
|
||||
| Logical proof | Building blocks assembling |
|
||||
| Uncertainty | Forking paths, fog, multiple shadows |
|
||||
|
||||
## Text Element Design
|
||||
|
||||
| Text Type | Style | Usage |
|
||||
|-----------|-------|-------|
|
||||
| Character dialogue | Oval speech bubble | Main narrative speech |
|
||||
| Narrator commentary | Rectangular box | Explanation, commentary |
|
||||
| Caption bar | Edge-mounted rectangle | Time, location info |
|
||||
| Thought bubble | Cloud shape | Character inner monologue |
|
||||
| Term label | Bold / special color | First appearance of technical terms |
|
||||
|
||||
## Prompt Structure for Consistency
|
||||
|
||||
Each page prompt should include character reference:
|
||||
|
||||
```
|
||||
[CHARACTER REFERENCE]
|
||||
(Key details from characters.md for characters in this page)
|
||||
|
||||
[PAGE CONTENT]
|
||||
(Specific scene, panel layout, and visual elements)
|
||||
|
||||
[CONSISTENCY REMINDER]
|
||||
Maintain exact character appearances as defined in character reference.
|
||||
- [Character A]: [key identifying features]
|
||||
- [Character B]: [key identifying features]
|
||||
```
|
||||
@@ -0,0 +1,66 @@
|
||||
# realistic
|
||||
|
||||
Full-color realistic manga style with digital painting techniques
|
||||
|
||||
## Style Guidelines
|
||||
|
||||
### Line Work
|
||||
- Clean, precise outlines with clear contours
|
||||
- Uniform line weight for character definition
|
||||
- No excessive hatching - rely on color for depth
|
||||
- Smooth curves and realistic anatomical lines
|
||||
- Ligne Claire influence: clean but not overly simplified
|
||||
|
||||
### Character Design
|
||||
- Realistic human proportions (7-8 head heights)
|
||||
- Anatomically accurate features and expressions
|
||||
- Detailed facial structure without exaggeration
|
||||
- Natural poses and body language
|
||||
- Consistent character appearance across all panels
|
||||
- Subtle expressions rather than manga-style exaggeration
|
||||
|
||||
### Rendering Style
|
||||
- Full-color digital painting with rich gradients
|
||||
- Soft shadow transitions on skin and fabric
|
||||
- Realistic material textures (glass, liquid, fabric, wood)
|
||||
- Detailed hair with natural shine and volume
|
||||
- Environmental lighting affects all elements
|
||||
- NOT flat cel-shading - use smooth color blending
|
||||
|
||||
### Background Treatment
|
||||
- Highly detailed, realistic environments
|
||||
- Accurate perspective and spatial depth
|
||||
- Atmospheric lighting (warm indoor, cool outdoor)
|
||||
- Professional settings rendered with precision
|
||||
- Props and objects with realistic textures
|
||||
|
||||
## Color Palette
|
||||
|
||||
- Skin: Natural warm tones (#F5D6C6, #E8C4B0)
|
||||
- Hair: Rich browns and blacks with highlights
|
||||
- Environment: Warm wood tones (#8B7355), cool stone (#9CA3AF)
|
||||
- Accent: Wine red (#722F37), gold (#D4AF37)
|
||||
- Lighting: Warm amber (#FFB347), cool blue (#B0C4DE)
|
||||
|
||||
## Color Approach
|
||||
- Rich gradients for depth and volume
|
||||
- Realistic lighting with warm/cool contrast
|
||||
- Material-specific rendering (glass transparency, liquid reflection)
|
||||
- Subtle color temperature shifts for atmosphere
|
||||
- Professional, sophisticated palette
|
||||
|
||||
## Quality Markers
|
||||
|
||||
A good realistic manga page exhibits:
|
||||
- ✓ Anatomically accurate character proportions
|
||||
- ✓ Smooth color gradients (not flat fills)
|
||||
- ✓ Realistic material textures
|
||||
- ✓ Detailed, atmospheric backgrounds
|
||||
- ✓ Natural lighting with soft shadows
|
||||
- ✓ Expressive but subtle facial expressions
|
||||
- ✓ Professional, sophisticated aesthetic
|
||||
- ✓ Clean speech bubbles with clear typography
|
||||
|
||||
## Best For
|
||||
|
||||
Professional topics (wine, food, business), lifestyle content, adult-oriented narratives, educational guides for mature audiences, documentary-style storytelling
|
||||
@@ -5,6 +5,13 @@ description: Image generation skill using Gemini Web. Generates images from text
|
||||
|
||||
# Gemini Web Client
|
||||
|
||||
Supports:
|
||||
- Text generation
|
||||
- Image generation (download + save)
|
||||
- Reference image upload (attach images for vision tasks)
|
||||
- Multi-turn conversations within the same executor instance (`keepSession`)
|
||||
- Experimental video generation (`generateVideo`) — Gemini may return an async placeholder; download might require Gemini web UI
|
||||
|
||||
## Quick start
|
||||
|
||||
```bash
|
||||
@@ -12,8 +19,25 @@ npx -y bun scripts/main.ts "Hello, Gemini"
|
||||
npx -y bun scripts/main.ts --prompt "Explain quantum computing"
|
||||
npx -y bun scripts/main.ts --prompt "A cute cat" --image cat.png
|
||||
npx -y bun scripts/main.ts --promptfiles system.md content.md --image out.png
|
||||
|
||||
# Multi-turn conversation (agent generates unique sessionId)
|
||||
npx -y bun scripts/main.ts "Remember this: 42" --sessionId my-unique-id-123
|
||||
npx -y bun scripts/main.ts "What number?" --sessionId my-unique-id-123
|
||||
```
|
||||
|
||||
## Executor options (programmatic)
|
||||
|
||||
This skill is typically consumed via `createGeminiWebExecutor(geminiOptions)` (see `scripts/executor.ts`).
|
||||
|
||||
Key options in `GeminiWebOptions`:
|
||||
- `referenceImages?: string | string[]` Upload local images as references (vision input).
|
||||
- `keepSession?: boolean` Reuse Gemini `chatMetadata` to continue the same conversation across calls (required if you want reference images to persist across multiple messages).
|
||||
- `generateVideo?: string` Generate a video and (best-effort) download to the given path. Gemini may return `video_gen_chip` (async); in that case you must open Gemini web UI to download the result.
|
||||
|
||||
Notes:
|
||||
- `generateVideo` cannot be combined with `generateImage` / `editImage`.
|
||||
- When `keepSession=true` and `referenceImages` is set, reference images are uploaded once per executor instance.
|
||||
|
||||
## Commands
|
||||
|
||||
### Text generation
|
||||
@@ -58,17 +82,21 @@ npx -y bun scripts/main.ts "Hello" --json
|
||||
|
||||
## Options
|
||||
|
||||
| Option | Short | Description |
|
||||
|--------|-------|-------------|
|
||||
| `--prompt <text>` | `-p` | Prompt text |
|
||||
| `--promptfiles <files...>` | | Read prompt from files (concatenated in order) |
|
||||
| `--model <id>` | `-m` | Model: gemini-3-pro (default), gemini-2.5-pro, gemini-2.5-flash |
|
||||
| `--image [path]` | | Generate image, save to path (default: generated.png) |
|
||||
| `--json` | | Output as JSON |
|
||||
| `--login` | | Refresh cookies only, then exit |
|
||||
| `--cookie-path <path>` | | Custom cookie file path |
|
||||
| `--profile-dir <path>` | | Chrome profile directory |
|
||||
| `--help` | `-h` | Show help |
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `--prompt <text>`, `-p` | Prompt text |
|
||||
| `--promptfiles <files...>` | Read prompt from files (concatenated in order) |
|
||||
| `--model <id>`, `-m` | Model: gemini-3-pro (default), gemini-2.5-pro, gemini-2.5-flash |
|
||||
| `--image [path]` | Generate image, save to path (default: generated.png) |
|
||||
| `--sessionId <id>` | Session ID for multi-turn conversation (agent generates unique ID) |
|
||||
| `--list-sessions` | List saved sessions (max 100, sorted by update time) |
|
||||
| `--json` | Output as JSON |
|
||||
| `--login` | Refresh cookies only, then exit |
|
||||
| `--cookie-path <path>` | Custom cookie file path |
|
||||
| `--profile-dir <path>` | Chrome profile directory |
|
||||
| `--help`, `-h` | Show help |
|
||||
|
||||
CLI note: `scripts/main.ts` supports text generation, image generation, and multi-turn conversations via `--sessionId`. Reference images and video generation are exposed via the executor API.
|
||||
|
||||
## Models
|
||||
|
||||
@@ -116,3 +144,22 @@ npx -y bun scripts/main.ts "Hello" --json | jq '.text'
|
||||
# Concatenate system.md + content.md as prompt
|
||||
npx -y bun scripts/main.ts --promptfiles system.md content.md --image output.png
|
||||
```
|
||||
|
||||
### Multi-turn conversation
|
||||
```bash
|
||||
# Start a session with unique ID (agent generates this)
|
||||
npx -y bun scripts/main.ts "You are a helpful math tutor." --sessionId task-abc123
|
||||
|
||||
# Continue the conversation (remembers context)
|
||||
npx -y bun scripts/main.ts "What is 2+2?" --sessionId task-abc123
|
||||
npx -y bun scripts/main.ts "Now multiply that by 10" --sessionId task-abc123
|
||||
|
||||
# List recent sessions (max 100, sorted by update time)
|
||||
npx -y bun scripts/main.ts --list-sessions
|
||||
```
|
||||
|
||||
Session files are stored in `~/Library/Application Support/baoyu-skills/gemini-web/sessions/<id>.json` and contain:
|
||||
- `id`: Session ID
|
||||
- `metadata`: Gemini chat metadata for continuation
|
||||
- `messages`: Array of `{role, content, timestamp, error?}`
|
||||
- `createdAt`, `updatedAt`: Timestamps
|
||||
|
||||
@@ -216,6 +216,7 @@ class CdpConnection {
|
||||
export async function getGeminiCookieMapViaChrome(options?: {
|
||||
timeoutMs?: number;
|
||||
debugConnectTimeoutMs?: number;
|
||||
tokenCheckTimeoutMs?: number;
|
||||
pollIntervalMs?: number;
|
||||
log?: GeminiWebLog;
|
||||
userDataDir?: string;
|
||||
@@ -224,6 +225,7 @@ export async function getGeminiCookieMapViaChrome(options?: {
|
||||
const log = options?.log;
|
||||
const timeoutMs = options?.timeoutMs ?? 5 * 60_000;
|
||||
const debugConnectTimeoutMs = options?.debugConnectTimeoutMs ?? 30_000;
|
||||
const tokenCheckTimeoutMs = options?.tokenCheckTimeoutMs ?? 30_000;
|
||||
const pollIntervalMs = options?.pollIntervalMs ?? 2_000;
|
||||
const userDataDir = options?.userDataDir ?? resolveGeminiWebChromeProfileDir();
|
||||
|
||||
@@ -290,7 +292,7 @@ export async function getGeminiCookieMapViaChrome(options?: {
|
||||
if (hasRequiredGeminiCookies(cookieMap)) {
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), 10_000);
|
||||
const timer = setTimeout(() => controller.abort(), tokenCheckTimeoutMs);
|
||||
try {
|
||||
await fetchGeminiAccessToken(cookieMap, controller.signal);
|
||||
} finally {
|
||||
|
||||
@@ -67,19 +67,69 @@ function buildCookieHeader(cookieMap: Record<string, string>): string {
|
||||
.join('; ');
|
||||
}
|
||||
|
||||
function getSetCookieHeaders(res: Response): string[] {
|
||||
const headers = res.headers as unknown as { getSetCookie?: () => string[] };
|
||||
if (typeof headers.getSetCookie === 'function') {
|
||||
try {
|
||||
return headers.getSetCookie();
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
const raw = res.headers.get('set-cookie');
|
||||
return raw ? [raw] : [];
|
||||
}
|
||||
|
||||
function applySetCookiesToMap(setCookies: string[], cookieMap: Record<string, string>): void {
|
||||
for (const raw of setCookies) {
|
||||
const first = raw.split(';')[0]?.trim();
|
||||
if (!first) continue;
|
||||
const idx = first.indexOf('=');
|
||||
if (idx <= 0) continue;
|
||||
const name = first.slice(0, idx).trim();
|
||||
const value = first.slice(idx + 1).trim();
|
||||
if (!name) continue;
|
||||
cookieMap[name] = value;
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchWithCookieJar(
|
||||
url: string,
|
||||
init: Omit<RequestInit, 'redirect' | 'headers'> & { headers?: Record<string, string> },
|
||||
cookieMap: Record<string, string>,
|
||||
signal?: AbortSignal,
|
||||
maxRedirects = 20,
|
||||
): Promise<Response> {
|
||||
let current = url;
|
||||
for (let i = 0; i <= maxRedirects; i += 1) {
|
||||
const cookieHeader = buildCookieHeader(cookieMap);
|
||||
const headers: Record<string, string> = {
|
||||
...(init.headers ?? {}),
|
||||
...(cookieHeader ? { cookie: cookieHeader } : {}),
|
||||
'user-agent': USER_AGENT,
|
||||
};
|
||||
|
||||
const res = await fetch(current, { ...init, redirect: 'manual', signal, headers });
|
||||
applySetCookiesToMap(getSetCookieHeaders(res), cookieMap);
|
||||
|
||||
if (res.status >= 300 && res.status < 400) {
|
||||
const location = res.headers.get('location');
|
||||
if (!location) return res;
|
||||
current = new URL(location, current).toString();
|
||||
continue;
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
throw new Error(`Too many redirects while fetching ${url} (>${maxRedirects}).`);
|
||||
}
|
||||
|
||||
export async function fetchGeminiAccessToken(
|
||||
cookieMap: Record<string, string>,
|
||||
signal?: AbortSignal,
|
||||
): Promise<string> {
|
||||
const cookieHeader = buildCookieHeader(cookieMap);
|
||||
const res = await fetch(GEMINI_APP_URL, {
|
||||
redirect: 'follow',
|
||||
signal,
|
||||
headers: {
|
||||
cookie: cookieHeader,
|
||||
'user-agent': USER_AGENT,
|
||||
},
|
||||
});
|
||||
const res = await fetchWithCookieJar(GEMINI_APP_URL, { method: 'GET' }, cookieMap, signal);
|
||||
const html = await res.text();
|
||||
|
||||
const tokens = ['SNlM0e', 'thykhd'] as const;
|
||||
@@ -107,7 +157,21 @@ function extractErrorCode(responseJson: unknown): number | undefined {
|
||||
}
|
||||
|
||||
function extractGgdlUrls(rawText: string): string[] {
|
||||
const matches = rawText.match(/https:\/\/lh3\.googleusercontent\.com\/gg-dl\/[^\s"']+/g) ?? [];
|
||||
const matches =
|
||||
rawText.match(/https?:\/\/[^/\s"']*googleusercontent\.com\/gg-dl\/[^\s"']+/g) ?? [];
|
||||
const seen = new Set<string>();
|
||||
const urls: string[] = [];
|
||||
for (const match of matches) {
|
||||
if (seen.has(match)) continue;
|
||||
seen.add(match);
|
||||
urls.push(match);
|
||||
}
|
||||
return urls;
|
||||
}
|
||||
|
||||
function extractImageGenerationContentUrls(rawText: string): string[] {
|
||||
const matches =
|
||||
rawText.match(/https?:\/\/googleusercontent\.com\/image_generation_content\/\d+/g) ?? [];
|
||||
const seen = new Set<string>();
|
||||
const urls: string[] = [];
|
||||
for (const match of matches) {
|
||||
@@ -119,9 +183,17 @@ function extractGgdlUrls(rawText: string): string[] {
|
||||
}
|
||||
|
||||
function ensureFullSizeImageUrl(url: string): string {
|
||||
if (url.includes('=s2048')) return url;
|
||||
if (url.includes('=s')) return url;
|
||||
return `${url}=s2048`;
|
||||
const trimmed = url.trim();
|
||||
let normalized = trimmed;
|
||||
const backslashIndex = normalized.indexOf('\\');
|
||||
if (backslashIndex >= 0) normalized = normalized.slice(0, backslashIndex);
|
||||
// Some Gemini responses embed a size suffix as "/=s2048" which breaks downloads.
|
||||
normalized = normalized.replace(/\/=s(?=\d+(?:$|[?#]))/, '=s');
|
||||
normalized = normalized.replace(/\/=s(?=$|[?#])/, '=s');
|
||||
if (normalized.endsWith('/')) normalized = normalized.slice(0, -1);
|
||||
if (normalized.includes('=s2048')) return normalized;
|
||||
if (normalized.includes('=s')) return normalized;
|
||||
return `${normalized}=s2048`;
|
||||
}
|
||||
|
||||
async function fetchWithCookiePreservingRedirects(
|
||||
@@ -190,6 +262,29 @@ async function uploadGeminiFile(filePath: string, signal?: AbortSignal): Promise
|
||||
return { id: text, name: fileName };
|
||||
}
|
||||
|
||||
function guessMimeType(fileName: string): string {
|
||||
const ext = path.extname(fileName).toLowerCase();
|
||||
switch (ext) {
|
||||
case '.png':
|
||||
return 'image/png';
|
||||
case '.jpg':
|
||||
case '.jpeg':
|
||||
return 'image/jpeg';
|
||||
case '.webp':
|
||||
return 'image/webp';
|
||||
case '.gif':
|
||||
return 'image/gif';
|
||||
case '.mp4':
|
||||
return 'video/mp4';
|
||||
case '.mov':
|
||||
return 'video/quicktime';
|
||||
case '.webm':
|
||||
return 'video/webm';
|
||||
default:
|
||||
return 'application/octet-stream';
|
||||
}
|
||||
}
|
||||
|
||||
function buildGeminiFReqPayload(
|
||||
prompt: string,
|
||||
uploaded: Array<{ id: string; name: string }>,
|
||||
@@ -201,9 +296,8 @@ function buildGeminiFReqPayload(
|
||||
prompt,
|
||||
0,
|
||||
null,
|
||||
// Matches gemini-webapi payload format: [[[fileId, 1]]] for a single attachment.
|
||||
// Keep it extensible for multiple uploads by emitting one [[id, 1]] entry per file.
|
||||
uploaded.map((file) => [[file.id, 1]]),
|
||||
// Matches gemini-web payload format: [[[fileId, 1, null, mimeType], fileName]] for an attachment.
|
||||
uploaded.map((file) => [[file.id, 1, null, guessMimeType(file.name)], file.name]),
|
||||
]
|
||||
: [prompt];
|
||||
|
||||
@@ -248,7 +342,19 @@ export function parseGeminiStreamGenerateResponse(rawText: string): {
|
||||
? (getNestedValue<string | null>(firstCandidate, [22, 0], null) ?? textRaw)
|
||||
: textRaw;
|
||||
const thoughts = getNestedValue<string | null>(firstCandidate, [37, 0, 0], null);
|
||||
const metadata = getNestedValue<unknown>(body, [1], []);
|
||||
const conversationMeta = getNestedValue<unknown[]>(body, [1], []);
|
||||
const conversationId =
|
||||
typeof conversationMeta[0] === 'string' && conversationMeta[0].length > 0
|
||||
? conversationMeta[0]
|
||||
: null;
|
||||
const responseId =
|
||||
typeof conversationMeta[1] === 'string' && conversationMeta[1].length > 0
|
||||
? conversationMeta[1]
|
||||
: null;
|
||||
const choiceIdRaw = getNestedValue<string | null>(firstCandidate, [0], null);
|
||||
const choiceId = typeof choiceIdRaw === 'string' && choiceIdRaw.length > 0 ? choiceIdRaw : null;
|
||||
const metadata =
|
||||
conversationId && responseId && choiceId ? [conversationId, responseId, choiceId] : conversationMeta;
|
||||
|
||||
const images: GeminiWebCandidateImage[] = [];
|
||||
|
||||
@@ -305,8 +411,8 @@ export function isGeminiModelUnavailable(errorCode: number | undefined): boolean
|
||||
}
|
||||
|
||||
export async function runGeminiWebOnce(input: GeminiWebRunInput): Promise<GeminiWebRunOutput> {
|
||||
const cookieHeader = buildCookieHeader(input.cookieMap);
|
||||
const at = await fetchGeminiAccessToken(input.cookieMap, input.signal);
|
||||
const cookieHeader = buildCookieHeader(input.cookieMap);
|
||||
|
||||
const uploaded: Array<{ id: string; name: string }> = [];
|
||||
for (const file of input.files ?? []) {
|
||||
@@ -403,11 +509,19 @@ export async function saveFirstGeminiImageFromOutput(
|
||||
return { saved: true, imageCount: output.images.length };
|
||||
}
|
||||
|
||||
const ggdl = extractGgdlUrls(output.rawResponseText);
|
||||
if (ggdl[0]) {
|
||||
await downloadGeminiImage(ggdl[0], cookieMap, outputPath, signal);
|
||||
const ggdl = extractGgdlUrls(`${output.text}\n${output.rawResponseText}`);
|
||||
const preferred = ggdl.length > 0 ? ggdl[ggdl.length - 1] : null;
|
||||
if (preferred) {
|
||||
await downloadGeminiImage(preferred, cookieMap, outputPath, signal);
|
||||
return { saved: true, imageCount: ggdl.length };
|
||||
}
|
||||
|
||||
const imageGen = extractImageGenerationContentUrls(`${output.text}\n${output.rawResponseText}`);
|
||||
const imageGenPreferred = imageGen.length > 0 ? imageGen[imageGen.length - 1] : null;
|
||||
if (imageGenPreferred) {
|
||||
await downloadGeminiImage(imageGenPreferred, cookieMap, outputPath, signal);
|
||||
return { saved: true, imageCount: imageGen.length };
|
||||
}
|
||||
|
||||
return { saved: false, imageCount: 0 };
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { mkdir, writeFile } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import type { BrowserRunOptions, BrowserRunResult, BrowserLogger, CookieParam } from '../browser/types.js';
|
||||
import { runGeminiWebWithFallback, saveFirstGeminiImageFromOutput } from './client.js';
|
||||
import type { GeminiWebModelId } from './client.js';
|
||||
import type { GeminiWebModelId, GeminiWebRunOutput } from './client.js';
|
||||
import {
|
||||
buildGeminiCookieMap,
|
||||
hasRequiredGeminiCookies,
|
||||
@@ -11,6 +12,9 @@ import type { GeminiWebOptions, GeminiWebResponse } from './types.js';
|
||||
|
||||
export { hasRequiredGeminiCookies } from './cookie-store.js';
|
||||
|
||||
const USER_AGENT =
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36';
|
||||
|
||||
function estimateTokenCount(text: string): number {
|
||||
return Math.ceil(text.length / 4);
|
||||
}
|
||||
@@ -22,6 +26,115 @@ function resolveInvocationPath(value: string | undefined): string | undefined {
|
||||
return path.isAbsolute(trimmed) ? trimmed : path.resolve(process.cwd(), trimmed);
|
||||
}
|
||||
|
||||
function normalizePathList(value: string | string[] | undefined): string[] {
|
||||
if (!value) return [];
|
||||
const raw = Array.isArray(value) ? value : [value];
|
||||
const out: string[] = [];
|
||||
for (const entry of raw) {
|
||||
if (typeof entry !== 'string') continue;
|
||||
const resolved = resolveInvocationPath(entry);
|
||||
if (!resolved) continue;
|
||||
out.push(resolved);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function dedupePaths(paths: string[]): string[] {
|
||||
const seen = new Set<string>();
|
||||
const out: string[] = [];
|
||||
for (const item of paths) {
|
||||
const trimmed = item.trim();
|
||||
if (!trimmed || seen.has(trimmed)) continue;
|
||||
seen.add(trimmed);
|
||||
out.push(trimmed);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function buildCookieHeader(cookieMap: Record<string, string>): string {
|
||||
return Object.entries(cookieMap)
|
||||
.filter(([, value]) => typeof value === 'string' && value.length > 0)
|
||||
.map(([name, value]) => `${name}=${value}`)
|
||||
.join('; ');
|
||||
}
|
||||
|
||||
async function fetchWithCookiePreservingRedirects(
|
||||
url: string,
|
||||
init: Omit<RequestInit, 'redirect'>,
|
||||
signal?: AbortSignal,
|
||||
maxRedirects = 10,
|
||||
): Promise<Response> {
|
||||
let current = url;
|
||||
for (let i = 0; i <= maxRedirects; i += 1) {
|
||||
const res = await fetch(current, { ...init, redirect: 'manual', signal });
|
||||
if (res.status >= 300 && res.status < 400) {
|
||||
const location = res.headers.get('location');
|
||||
if (!location) return res;
|
||||
current = new URL(location, current).toString();
|
||||
continue;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
throw new Error(`Too many redirects while downloading media (>${maxRedirects}).`);
|
||||
}
|
||||
|
||||
async function downloadGeminiMedia(
|
||||
url: string,
|
||||
cookieMap: Record<string, string>,
|
||||
outputPath: string,
|
||||
signal?: AbortSignal,
|
||||
): Promise<void> {
|
||||
const cookieHeader = buildCookieHeader(cookieMap);
|
||||
const res = await fetchWithCookiePreservingRedirects(
|
||||
url,
|
||||
{
|
||||
headers: {
|
||||
cookie: cookieHeader,
|
||||
'user-agent': USER_AGENT,
|
||||
},
|
||||
},
|
||||
signal,
|
||||
);
|
||||
if (!res.ok) {
|
||||
throw new Error(`Failed to download media: ${res.status} ${res.statusText} (${res.url})`);
|
||||
}
|
||||
|
||||
const data = new Uint8Array(await res.arrayBuffer());
|
||||
await mkdir(path.dirname(outputPath), { recursive: true });
|
||||
await writeFile(outputPath, data);
|
||||
}
|
||||
|
||||
function extractGgdlUrls(rawText: string): string[] {
|
||||
const matches =
|
||||
rawText.match(/https?:\/\/[^/\s"']*googleusercontent\.com\/gg-dl\/[^\s"']+/g) ?? [];
|
||||
const seen = new Set<string>();
|
||||
const urls: string[] = [];
|
||||
for (const match of matches) {
|
||||
if (seen.has(match)) continue;
|
||||
seen.add(match);
|
||||
urls.push(match);
|
||||
}
|
||||
return urls;
|
||||
}
|
||||
|
||||
async function saveFirstGeminiVideoFromOutput(
|
||||
output: GeminiWebRunOutput,
|
||||
cookieMap: Record<string, string>,
|
||||
outputPath: string,
|
||||
signal?: AbortSignal,
|
||||
): Promise<{ saved: boolean; videoCount: number }> {
|
||||
const ggdl = extractGgdlUrls(output.rawResponseText);
|
||||
if (!ggdl[0]) return { saved: false, videoCount: 0 };
|
||||
|
||||
const videoCandidates = ggdl.filter((url) => /\.(mp4|webm|mov)(?:$|[?#])/i.test(url));
|
||||
const preferred =
|
||||
(videoCandidates.length > 0 ? videoCandidates[videoCandidates.length - 1] : null) ??
|
||||
ggdl.find((url) => /video/i.test(url)) ??
|
||||
ggdl[ggdl.length - 1];
|
||||
await downloadGeminiMedia(preferred, cookieMap, outputPath, signal);
|
||||
return { saved: true, videoCount: ggdl.length };
|
||||
}
|
||||
|
||||
function resolveGeminiWebModel(
|
||||
desiredModel: string | null | undefined,
|
||||
log?: BrowserLogger,
|
||||
@@ -115,6 +228,9 @@ export async function loadGeminiCookieMap(log?: BrowserLogger): Promise<Record<s
|
||||
export function createGeminiWebExecutor(
|
||||
geminiOptions: GeminiWebOptions,
|
||||
): (runOptions: BrowserRunOptions) => Promise<BrowserRunResult> {
|
||||
let persistedChatMetadata: unknown | null = null;
|
||||
let referenceImagesUploaded = false;
|
||||
|
||||
return async (runOptions: BrowserRunOptions): Promise<BrowserRunResult> => {
|
||||
const startTime = Date.now();
|
||||
const log = runOptions.log;
|
||||
@@ -133,23 +249,38 @@ export function createGeminiWebExecutor(
|
||||
? Math.max(1_000, runOptions.config.timeoutMs)
|
||||
: null;
|
||||
|
||||
const generateVideoPath = resolveInvocationPath(geminiOptions.generateVideo);
|
||||
|
||||
const defaultTimeoutMs = geminiOptions.youtube
|
||||
? 240_000
|
||||
: geminiOptions.generateImage || geminiOptions.editImage
|
||||
: generateVideoPath
|
||||
? 900_000
|
||||
: geminiOptions.generateImage || geminiOptions.editImage
|
||||
? 300_000
|
||||
: 120_000;
|
||||
|
||||
const timeoutMs = Math.min(configTimeout ?? defaultTimeoutMs, 600_000);
|
||||
const timeoutCapMs = generateVideoPath ? 1_800_000 : 600_000;
|
||||
const timeoutMs = Math.min(configTimeout ?? defaultTimeoutMs, timeoutCapMs);
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
||||
|
||||
const keepSession = geminiOptions.keepSession === true;
|
||||
|
||||
const generateImagePath = resolveInvocationPath(geminiOptions.generateImage);
|
||||
const editImagePath = resolveInvocationPath(geminiOptions.editImage);
|
||||
const outputPath = resolveInvocationPath(geminiOptions.outputPath);
|
||||
const attachmentPaths = (runOptions.attachments ?? []).map((attachment) => attachment.path);
|
||||
const referenceImagePaths = normalizePathList(geminiOptions.referenceImages);
|
||||
const requestFilePaths = dedupePaths(
|
||||
keepSession ? attachmentPaths : [...referenceImagePaths, ...attachmentPaths],
|
||||
);
|
||||
|
||||
if (generateVideoPath && (generateImagePath || editImagePath)) {
|
||||
throw new Error('Gemini web executor: generateVideo cannot be combined with generateImage/editImage options.');
|
||||
}
|
||||
|
||||
let prompt = runOptions.prompt;
|
||||
if (geminiOptions.aspectRatio && (generateImagePath || editImagePath)) {
|
||||
if (geminiOptions.aspectRatio && (generateImagePath || editImagePath || generateVideoPath)) {
|
||||
prompt = `${prompt} (aspect ratio: ${geminiOptions.aspectRatio})`;
|
||||
}
|
||||
if (geminiOptions.youtube) {
|
||||
@@ -158,29 +289,50 @@ export function createGeminiWebExecutor(
|
||||
if (generateImagePath && !editImagePath) {
|
||||
prompt = `Generate an image: ${prompt}`;
|
||||
}
|
||||
if (generateVideoPath) {
|
||||
prompt = `Generate a video: ${prompt}`;
|
||||
}
|
||||
|
||||
const model: GeminiWebModelId = resolveGeminiWebModel(runOptions.config?.desiredModel, log);
|
||||
let response: GeminiWebResponse;
|
||||
let videoSaveSummary: { saved: boolean; videoCount: number; outputPath: string } | null = null;
|
||||
|
||||
try {
|
||||
let chatMetadata: unknown = keepSession ? persistedChatMetadata : null;
|
||||
|
||||
if (keepSession && referenceImagePaths.length > 0 && !referenceImagesUploaded) {
|
||||
const intro = await runGeminiWebWithFallback({
|
||||
prompt: 'Here are reference images for future messages.',
|
||||
files: referenceImagePaths,
|
||||
model,
|
||||
cookieMap,
|
||||
chatMetadata,
|
||||
signal: controller.signal,
|
||||
});
|
||||
chatMetadata = intro.metadata;
|
||||
persistedChatMetadata = intro.metadata;
|
||||
referenceImagesUploaded = true;
|
||||
}
|
||||
|
||||
if (editImagePath) {
|
||||
const intro = await runGeminiWebWithFallback({
|
||||
prompt: 'Here is an image to edit',
|
||||
files: [editImagePath],
|
||||
model,
|
||||
cookieMap,
|
||||
chatMetadata: null,
|
||||
chatMetadata,
|
||||
signal: controller.signal,
|
||||
});
|
||||
const editPrompt = `Use image generation tool to ${prompt}`;
|
||||
const out = await runGeminiWebWithFallback({
|
||||
prompt: editPrompt,
|
||||
files: attachmentPaths,
|
||||
files: requestFilePaths,
|
||||
model,
|
||||
cookieMap,
|
||||
chatMetadata: intro.metadata,
|
||||
signal: controller.signal,
|
||||
});
|
||||
if (keepSession) persistedChatMetadata = out.metadata;
|
||||
response = {
|
||||
text: out.text ?? null,
|
||||
thoughts: geminiOptions.showThoughts ? out.thoughts : null,
|
||||
@@ -198,12 +350,13 @@ export function createGeminiWebExecutor(
|
||||
} else if (generateImagePath) {
|
||||
const out = await runGeminiWebWithFallback({
|
||||
prompt,
|
||||
files: attachmentPaths,
|
||||
files: requestFilePaths,
|
||||
model,
|
||||
cookieMap,
|
||||
chatMetadata: null,
|
||||
chatMetadata,
|
||||
signal: controller.signal,
|
||||
});
|
||||
if (keepSession) persistedChatMetadata = out.metadata;
|
||||
response = {
|
||||
text: out.text ?? null,
|
||||
thoughts: geminiOptions.showThoughts ? out.thoughts : null,
|
||||
@@ -216,15 +369,36 @@ export function createGeminiWebExecutor(
|
||||
if (!imageSave.saved) {
|
||||
throw new Error(`No images generated. Response text:\n${out.text || '(empty response)'}`);
|
||||
}
|
||||
} else if (generateVideoPath) {
|
||||
const out = await runGeminiWebWithFallback({
|
||||
prompt,
|
||||
files: requestFilePaths,
|
||||
model,
|
||||
cookieMap,
|
||||
chatMetadata,
|
||||
signal: controller.signal,
|
||||
});
|
||||
if (keepSession) persistedChatMetadata = out.metadata;
|
||||
response = {
|
||||
text: out.text ?? null,
|
||||
thoughts: geminiOptions.showThoughts ? out.thoughts : null,
|
||||
has_images: false,
|
||||
image_count: 0,
|
||||
};
|
||||
|
||||
const resolvedOutputPath = generateVideoPath ?? outputPath ?? 'generated.mp4';
|
||||
const save = await saveFirstGeminiVideoFromOutput(out, cookieMap, resolvedOutputPath, controller.signal);
|
||||
videoSaveSummary = { ...save, outputPath: resolvedOutputPath };
|
||||
} else {
|
||||
const out = await runGeminiWebWithFallback({
|
||||
prompt,
|
||||
files: attachmentPaths,
|
||||
files: requestFilePaths,
|
||||
model,
|
||||
cookieMap,
|
||||
chatMetadata: null,
|
||||
chatMetadata,
|
||||
signal: controller.signal,
|
||||
});
|
||||
if (keepSession) persistedChatMetadata = out.metadata;
|
||||
response = {
|
||||
text: out.text ?? null,
|
||||
thoughts: geminiOptions.showThoughts ? out.thoughts : null,
|
||||
@@ -247,6 +421,15 @@ export function createGeminiWebExecutor(
|
||||
const imagePath = generateImagePath || outputPath || 'generated.png';
|
||||
answerMarkdown += `\n\n*Generated ${response.image_count} image(s). Saved to: ${imagePath}*`;
|
||||
}
|
||||
if (videoSaveSummary) {
|
||||
if (videoSaveSummary.saved) {
|
||||
answerMarkdown += `\n\n*Generated ${videoSaveSummary.videoCount || 1} video(s). Saved to: ${videoSaveSummary.outputPath}*`;
|
||||
} else if (/video_gen_chip/.test(answerMarkdown) || /video_gen_chip/.test(response.text ?? '')) {
|
||||
answerMarkdown += '\n\n*Video generation is asynchronous. Check Gemini web UI to download the result.*';
|
||||
} else {
|
||||
answerMarkdown += '\n\n*No downloadable video URL found in Gemini response.*';
|
||||
}
|
||||
}
|
||||
|
||||
const tookMs = Date.now() - startTime;
|
||||
log?.(`[gemini-web] Completed in ${tookMs}ms`);
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
writeGeminiCookieMapToDisk,
|
||||
} from './cookie-store.js';
|
||||
import { resolveGeminiWebChromeProfileDir, resolveGeminiWebCookiePath } from './paths.js';
|
||||
import { readSession, writeSession, listSessions } from './session-store.js';
|
||||
|
||||
function printUsage(exitCode = 0): never {
|
||||
const cookiePath = resolveGeminiWebCookiePath();
|
||||
@@ -21,12 +22,19 @@ function printUsage(exitCode = 0): never {
|
||||
npx -y bun skills/baoyu-gemini-web/scripts/main.ts --prompt "A cute cat" --image generated.png
|
||||
npx -y bun skills/baoyu-gemini-web/scripts/main.ts --promptfiles system.md content.md --image out.png
|
||||
|
||||
Multi-turn conversation (agent generates unique sessionId):
|
||||
npx -y bun skills/baoyu-gemini-web/scripts/main.ts "Remember 42" --sessionId abc123
|
||||
npx -y bun skills/baoyu-gemini-web/scripts/main.ts "What number?" --sessionId abc123
|
||||
|
||||
Options:
|
||||
-p, --prompt <text> Prompt text
|
||||
--promptfiles <files...> Read prompt from one or more files (concatenated in order)
|
||||
-m, --model <id> gemini-3-pro | gemini-2.5-pro | gemini-2.5-flash (default: gemini-3-pro)
|
||||
--json Output JSON
|
||||
--image [path] Generate an image and save it (default: ./generated.png)
|
||||
--reference <files...> Reference images for vision input
|
||||
--sessionId <id> Session ID for multi-turn conversation (agent should generate unique ID)
|
||||
--list-sessions List saved sessions (max 100, sorted by update time)
|
||||
--login Only refresh cookies, then exit
|
||||
--cookie-path <path> Cookie file path (default: ${cookiePath})
|
||||
--profile-dir <path> Chrome profile dir (default: ${profileDir})
|
||||
@@ -75,6 +83,9 @@ function parseArgs(argv: string[]): {
|
||||
loginOnly?: boolean;
|
||||
cookiePath?: string;
|
||||
profileDir?: string;
|
||||
referenceImages?: string[];
|
||||
sessionId?: string;
|
||||
listSessions?: boolean;
|
||||
} {
|
||||
const out: ReturnType<typeof parseArgs> = {};
|
||||
const positional: string[] = [];
|
||||
@@ -157,6 +168,32 @@ function parseArgs(argv: string[]): {
|
||||
out.profileDir = arg.slice('--profile-dir='.length);
|
||||
continue;
|
||||
}
|
||||
if (arg === '--reference' || arg === '--ref') {
|
||||
out.referenceImages = [];
|
||||
while (i + 1 < argv.length) {
|
||||
const next = argv[i + 1];
|
||||
if (next && !next.startsWith('-')) {
|
||||
out.referenceImages.push(next);
|
||||
i += 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (arg === '--sessionId' || arg === '--session-id') {
|
||||
out.sessionId = argv[i + 1] ?? '';
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
if (arg.startsWith('--sessionId=') || arg.startsWith('--session-id=')) {
|
||||
out.sessionId = arg.split('=')[1] ?? '';
|
||||
continue;
|
||||
}
|
||||
if (arg === '--list-sessions') {
|
||||
out.listSessions = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg.startsWith('-')) {
|
||||
throw new Error(`Unknown option: ${arg}`);
|
||||
@@ -178,6 +215,9 @@ function parseArgs(argv: string[]): {
|
||||
if (out.cookiePath === '') delete out.cookiePath;
|
||||
if (out.profileDir === '') delete out.profileDir;
|
||||
if (out.promptFiles?.length === 0) delete out.promptFiles;
|
||||
if (out.referenceImages?.length === 0) delete out.referenceImages;
|
||||
if (out.sessionId != null) out.sessionId = out.sessionId.trim();
|
||||
if (out.sessionId === '') delete out.sessionId;
|
||||
|
||||
return out;
|
||||
}
|
||||
@@ -186,7 +226,7 @@ async function isCookieMapValid(cookieMap: Record<string, string>): Promise<bool
|
||||
if (!hasRequiredGeminiCookies(cookieMap)) return false;
|
||||
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), 10_000);
|
||||
const timer = setTimeout(() => controller.abort(), 30_000);
|
||||
try {
|
||||
await fetchGeminiAccessToken(cookieMap, controller.signal);
|
||||
return true;
|
||||
@@ -201,7 +241,7 @@ async function ensureGeminiCookieMap(options: {
|
||||
cookiePath: string;
|
||||
profileDir: string;
|
||||
}): Promise<Record<string, string>> {
|
||||
const log = (msg: string) => console.log(msg);
|
||||
const log = (msg: string) => console.error(msg);
|
||||
|
||||
let cookieMap = await readGeminiCookieMapFromDisk({ cookiePath: options.cookiePath, log });
|
||||
if (await isCookieMapValid(cookieMap)) return cookieMap;
|
||||
@@ -251,85 +291,63 @@ async function main(): Promise<void> {
|
||||
const cookiePath = args.cookiePath ?? resolveGeminiWebCookiePath();
|
||||
const profileDir = args.profileDir ?? resolveGeminiWebChromeProfileDir();
|
||||
|
||||
if (args.listSessions) {
|
||||
const sessions = await listSessions();
|
||||
if (sessions.length === 0) {
|
||||
console.log('No saved sessions.');
|
||||
} else {
|
||||
for (const { id, updatedAt } of sessions) {
|
||||
console.log(`${id}\t${updatedAt}`);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.loginOnly) {
|
||||
await ensureGeminiCookieMap({ cookiePath, profileDir });
|
||||
return;
|
||||
}
|
||||
|
||||
const promptFromStdin = await readPromptFromStdin();
|
||||
const promptFromFiles = args.promptFiles ? readPromptFiles(args.promptFiles) : null;
|
||||
const prompt = promptFromFiles || args.prompt || promptFromStdin;
|
||||
const promptFromArgs = promptFromFiles || args.prompt;
|
||||
const prompt = promptFromArgs || (await readPromptFromStdin());
|
||||
if (!prompt) printUsage(1);
|
||||
|
||||
const sessionData = args.sessionId ? await readSession(args.sessionId) : null;
|
||||
const chatMetadata = sessionData?.metadata ?? null;
|
||||
|
||||
let cookieMap = await ensureGeminiCookieMap({ cookiePath, profileDir });
|
||||
|
||||
const desiredModel = resolveModel(args.model || 'gemini-3-pro');
|
||||
const imagePath = resolveImageOutputPath(args.imagePath);
|
||||
const referenceImages = (args.referenceImages ?? []).map((p) =>
|
||||
path.isAbsolute(p) ? p : path.resolve(process.cwd(), p),
|
||||
);
|
||||
|
||||
try {
|
||||
const effectivePrompt = imagePath ? `Generate an image: ${prompt}` : prompt;
|
||||
const out = await runGeminiWebWithFallback({
|
||||
prompt: effectivePrompt,
|
||||
files: [],
|
||||
model: desiredModel,
|
||||
cookieMap,
|
||||
chatMetadata: null,
|
||||
});
|
||||
|
||||
let imageSaved = false;
|
||||
let imageCount = 0;
|
||||
if (imagePath) {
|
||||
const save = await saveFirstGeminiImageFromOutput(out, cookieMap, imagePath);
|
||||
imageSaved = save.saved;
|
||||
imageCount = save.imageCount;
|
||||
if (!imageSaved) {
|
||||
throw new Error(`No images generated. Response text:\n${out.text || '(empty response)'}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (args.json) {
|
||||
process.stdout.write(
|
||||
`${JSON.stringify(
|
||||
imagePath ? { ...out, imageSaved, imageCount, imagePath } : out,
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
);
|
||||
if (out.errorMessage) process.exit(1);
|
||||
return;
|
||||
}
|
||||
|
||||
if (out.errorMessage) {
|
||||
throw new Error(out.errorMessage);
|
||||
}
|
||||
|
||||
process.stdout.write(out.text ?? '');
|
||||
if (!out.text?.endsWith('\n')) process.stdout.write('\n');
|
||||
if (imagePath) {
|
||||
process.stdout.write(`Saved image (${imageCount || 1}) to: ${imagePath}\n`);
|
||||
}
|
||||
return;
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
|
||||
if (message.includes('Unable to locate Gemini access token')) {
|
||||
console.error('[gemini-web] Cookies may be expired. Re-opening browser to refresh cookies...');
|
||||
await sleep(500);
|
||||
cookieMap = await getGeminiCookieMapViaChrome({ userDataDir: profileDir, log: (m) => console.log(m) });
|
||||
await writeGeminiCookieMapToDisk(cookieMap, { cookiePath, log: (m) => console.log(m) });
|
||||
const controller = new AbortController();
|
||||
const timeoutMs = imagePath ? 300_000 : 120_000;
|
||||
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
||||
|
||||
try {
|
||||
const effectivePrompt = imagePath ? `Generate an image: ${prompt}` : prompt;
|
||||
const out = await runGeminiWebWithFallback({
|
||||
prompt: imagePath ? `Generate an image: ${prompt}` : prompt,
|
||||
files: [],
|
||||
prompt: effectivePrompt,
|
||||
files: referenceImages,
|
||||
model: desiredModel,
|
||||
cookieMap,
|
||||
chatMetadata: null,
|
||||
chatMetadata,
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
if (args.sessionId && out.metadata) {
|
||||
await writeSession(args.sessionId, out.metadata, prompt, out.text ?? '', out.errorMessage);
|
||||
}
|
||||
|
||||
let imageSaved = false;
|
||||
let imageCount = 0;
|
||||
if (imagePath) {
|
||||
const save = await saveFirstGeminiImageFromOutput(out, cookieMap, imagePath);
|
||||
const save = await saveFirstGeminiImageFromOutput(out, cookieMap, imagePath, controller.signal);
|
||||
imageSaved = save.saved;
|
||||
imageCount = save.imageCount;
|
||||
if (!imageSaved) {
|
||||
@@ -338,13 +356,8 @@ async function main(): Promise<void> {
|
||||
}
|
||||
|
||||
if (args.json) {
|
||||
process.stdout.write(
|
||||
`${JSON.stringify(
|
||||
imagePath ? { ...out, imageSaved, imageCount, imagePath } : out,
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
);
|
||||
const jsonOut = { ...out, ...(imagePath && { imageSaved, imageCount, imagePath }), ...(args.sessionId && { sessionId: args.sessionId }) };
|
||||
process.stdout.write(`${JSON.stringify(jsonOut, null, 2)}\n`);
|
||||
if (out.errorMessage) process.exit(1);
|
||||
return;
|
||||
}
|
||||
@@ -359,6 +372,67 @@ async function main(): Promise<void> {
|
||||
process.stdout.write(`Saved image (${imageCount || 1}) to: ${imagePath}\n`);
|
||||
}
|
||||
return;
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
|
||||
if (message.includes('Unable to locate Gemini access token')) {
|
||||
console.error('[gemini-web] Cookies may be expired. Re-opening browser to refresh cookies...');
|
||||
await sleep(500);
|
||||
cookieMap = await getGeminiCookieMapViaChrome({ userDataDir: profileDir, log: (m) => console.error(m) });
|
||||
await writeGeminiCookieMapToDisk(cookieMap, { cookiePath, log: (m) => console.error(m) });
|
||||
|
||||
const controller = new AbortController();
|
||||
const timeoutMs = imagePath ? 300_000 : 120_000;
|
||||
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
||||
|
||||
try {
|
||||
const out = await runGeminiWebWithFallback({
|
||||
prompt: imagePath ? `Generate an image: ${prompt}` : prompt,
|
||||
files: referenceImages,
|
||||
model: desiredModel,
|
||||
cookieMap,
|
||||
chatMetadata,
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
if (args.sessionId && out.metadata) {
|
||||
await writeSession(args.sessionId, out.metadata, prompt, out.text ?? '', out.errorMessage);
|
||||
}
|
||||
|
||||
let imageSaved = false;
|
||||
let imageCount = 0;
|
||||
if (imagePath) {
|
||||
const save = await saveFirstGeminiImageFromOutput(out, cookieMap, imagePath, controller.signal);
|
||||
imageSaved = save.saved;
|
||||
imageCount = save.imageCount;
|
||||
if (!imageSaved) {
|
||||
throw new Error(`No images generated. Response text:\n${out.text || '(empty response)'}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (args.json) {
|
||||
const jsonOut = { ...out, ...(imagePath && { imageSaved, imageCount, imagePath }), ...(args.sessionId && { sessionId: args.sessionId }) };
|
||||
process.stdout.write(`${JSON.stringify(jsonOut, null, 2)}\n`);
|
||||
if (out.errorMessage) process.exit(1);
|
||||
return;
|
||||
}
|
||||
|
||||
if (out.errorMessage) {
|
||||
throw new Error(out.errorMessage);
|
||||
}
|
||||
|
||||
process.stdout.write(out.text ?? '');
|
||||
if (!out.text?.endsWith('\n')) process.stdout.write('\n');
|
||||
if (imagePath) {
|
||||
process.stdout.write(`Saved image (${imageCount || 1}) to: ${imagePath}\n`);
|
||||
}
|
||||
return;
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
}
|
||||
|
||||
throw error;
|
||||
|
||||
@@ -34,3 +34,12 @@ export function resolveGeminiWebChromeProfileDir(): string {
|
||||
if (override) return path.resolve(override);
|
||||
return path.join(resolveGeminiWebDataDir(), PROFILE_DIR_NAME);
|
||||
}
|
||||
|
||||
export function resolveGeminiWebSessionsDir(): string {
|
||||
return path.join(resolveGeminiWebDataDir(), 'sessions');
|
||||
}
|
||||
|
||||
export function resolveGeminiWebSessionPath(name: string): string {
|
||||
const sanitized = name.replace(/[^a-zA-Z0-9_-]/g, '_');
|
||||
return path.join(resolveGeminiWebSessionsDir(), `${sanitized}.json`);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import { mkdir, readFile, writeFile, readdir, stat } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { resolveGeminiWebSessionsDir, resolveGeminiWebSessionPath } from './paths.js';
|
||||
|
||||
export interface SessionMessage {
|
||||
role: 'user' | 'assistant';
|
||||
content: string;
|
||||
timestamp: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface SessionData {
|
||||
id: string;
|
||||
metadata: unknown;
|
||||
messages: SessionMessage[];
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface SessionListItem {
|
||||
id: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export async function readSession(id: string): Promise<SessionData | null> {
|
||||
const sessionPath = resolveGeminiWebSessionPath(id);
|
||||
try {
|
||||
const content = await readFile(sessionPath, 'utf8');
|
||||
return JSON.parse(content) as SessionData;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function writeSession(
|
||||
id: string,
|
||||
metadata: unknown,
|
||||
userMessage: string,
|
||||
assistantMessage: string,
|
||||
error?: string,
|
||||
): Promise<void> {
|
||||
const sessionPath = resolveGeminiWebSessionPath(id);
|
||||
const sessionsDir = resolveGeminiWebSessionsDir();
|
||||
await mkdir(sessionsDir, { recursive: true });
|
||||
|
||||
const existing = await readSession(id);
|
||||
const now = new Date().toISOString();
|
||||
|
||||
const newMessages: SessionMessage[] = [
|
||||
{ role: 'user', content: userMessage, timestamp: now },
|
||||
{ role: 'assistant', content: assistantMessage, timestamp: now, ...(error && { error }) },
|
||||
];
|
||||
|
||||
const data: SessionData = {
|
||||
id,
|
||||
metadata,
|
||||
messages: [...(existing?.messages ?? []), ...newMessages],
|
||||
createdAt: existing?.createdAt ?? now,
|
||||
updatedAt: now,
|
||||
};
|
||||
await writeFile(sessionPath, JSON.stringify(data, null, 2));
|
||||
}
|
||||
|
||||
export async function listSessions(limit = 100): Promise<SessionListItem[]> {
|
||||
const sessionsDir = resolveGeminiWebSessionsDir();
|
||||
try {
|
||||
const files = await readdir(sessionsDir);
|
||||
const jsonFiles = files.filter((f) => f.endsWith('.json'));
|
||||
|
||||
const items: { id: string; updatedAt: string; mtime: number }[] = [];
|
||||
for (const file of jsonFiles) {
|
||||
const filePath = path.join(sessionsDir, file);
|
||||
try {
|
||||
const stats = await stat(filePath);
|
||||
items.push({
|
||||
id: file.slice(0, -5),
|
||||
updatedAt: stats.mtime.toISOString(),
|
||||
mtime: stats.mtime.getTime(),
|
||||
});
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
items.sort((a, b) => b.mtime - a.mtime);
|
||||
return items.slice(0, limit).map(({ id, updatedAt }) => ({ id, updatedAt }));
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -2,9 +2,18 @@ export interface GeminiWebOptions {
|
||||
youtube?: string;
|
||||
generateImage?: string;
|
||||
editImage?: string;
|
||||
generateVideo?: string;
|
||||
outputPath?: string;
|
||||
showThoughts?: boolean;
|
||||
aspectRatio?: string;
|
||||
/**
|
||||
* One or more local image paths to upload as persistent reference images.
|
||||
* - If `keepSession` is enabled, they are uploaded once per executor session.
|
||||
* - Otherwise, they are attached to each request.
|
||||
*/
|
||||
referenceImages?: string | string[];
|
||||
/** Preserve Gemini chat metadata to continue multi-turn conversations within the same executor instance. */
|
||||
keepSession?: boolean;
|
||||
}
|
||||
|
||||
export interface GeminiWebResponse {
|
||||
|
||||
@@ -481,52 +481,78 @@ export async function publishArticle(options: ArticleOptions): Promise<void> {
|
||||
const img = sortedImages[i]!;
|
||||
console.log(`[x-article] [${i + 1}/${sortedImages.length}] Inserting image at placeholder: ${img.placeholder}`);
|
||||
|
||||
// Find, scroll to, and select the placeholder text in DraftEditor
|
||||
const placeholderFound = await cdp.send<{ result: { value: boolean } }>('Runtime.evaluate', {
|
||||
expression: `(() => {
|
||||
const editor = document.querySelector('.DraftEditor-editorContainer [data-contents="true"]');
|
||||
if (!editor) return false;
|
||||
// Helper to select placeholder with retry
|
||||
const selectPlaceholder = async (maxRetries = 3): Promise<boolean> => {
|
||||
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||||
// Find, scroll to, and select the placeholder text in DraftEditor
|
||||
await cdp!.send('Runtime.evaluate', {
|
||||
expression: `(() => {
|
||||
const editor = document.querySelector('.DraftEditor-editorContainer [data-contents="true"]');
|
||||
if (!editor) return false;
|
||||
|
||||
const placeholder = ${JSON.stringify(img.placeholder)};
|
||||
const placeholder = ${JSON.stringify(img.placeholder)};
|
||||
|
||||
// Search through all text nodes in the editor
|
||||
const walker = document.createTreeWalker(editor, NodeFilter.SHOW_TEXT, null, false);
|
||||
let node;
|
||||
// Search through all text nodes in the editor
|
||||
const walker = document.createTreeWalker(editor, NodeFilter.SHOW_TEXT, null, false);
|
||||
let node;
|
||||
|
||||
while ((node = walker.nextNode())) {
|
||||
const text = node.textContent || '';
|
||||
const idx = text.indexOf(placeholder);
|
||||
if (idx !== -1) {
|
||||
// Found the placeholder - scroll to it first
|
||||
const parentElement = node.parentElement;
|
||||
if (parentElement) {
|
||||
parentElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
while ((node = walker.nextNode())) {
|
||||
const text = node.textContent || '';
|
||||
const idx = text.indexOf(placeholder);
|
||||
if (idx !== -1) {
|
||||
// Found the placeholder - scroll to it first
|
||||
const parentElement = node.parentElement;
|
||||
if (parentElement) {
|
||||
parentElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
}
|
||||
|
||||
// Select it
|
||||
const range = document.createRange();
|
||||
range.setStart(node, idx);
|
||||
range.setEnd(node, idx + placeholder.length);
|
||||
const sel = window.getSelection();
|
||||
sel.removeAllRanges();
|
||||
sel.addRange(range);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
})()`,
|
||||
}, { sessionId });
|
||||
|
||||
// Select it
|
||||
const range = document.createRange();
|
||||
range.setStart(node, idx);
|
||||
range.setEnd(node, idx + placeholder.length);
|
||||
const sel = window.getSelection();
|
||||
sel.removeAllRanges();
|
||||
sel.addRange(range);
|
||||
return true;
|
||||
}
|
||||
// Wait for scroll and selection to settle
|
||||
await sleep(800);
|
||||
|
||||
// Verify selection matches the placeholder
|
||||
const selectionCheck = await cdp!.send<{ result: { value: string } }>('Runtime.evaluate', {
|
||||
expression: `window.getSelection()?.toString() || ''`,
|
||||
returnByValue: true,
|
||||
}, { sessionId });
|
||||
|
||||
const selectedText = selectionCheck.result.value.trim();
|
||||
if (selectedText === img.placeholder) {
|
||||
console.log(`[x-article] Selection verified: "${selectedText}"`);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
})()`,
|
||||
returnByValue: true,
|
||||
}, { sessionId });
|
||||
|
||||
// Wait for scroll animation
|
||||
await sleep(500);
|
||||
if (attempt < maxRetries) {
|
||||
console.log(`[x-article] Selection attempt ${attempt} got "${selectedText}", retrying...`);
|
||||
await sleep(500);
|
||||
} else {
|
||||
console.warn(`[x-article] Selection failed after ${maxRetries} attempts, got: "${selectedText}"`);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
if (!placeholderFound.result.value) {
|
||||
console.warn(`[x-article] Placeholder not found in DOM: ${img.placeholder}`);
|
||||
// Try to select the placeholder
|
||||
const selected = await selectPlaceholder(3);
|
||||
if (!selected) {
|
||||
console.warn(`[x-article] Skipping image - could not select placeholder: ${img.placeholder}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
console.log(`[x-article] Placeholder selected, copying image: ${path.basename(img.localPath)}`);
|
||||
console.log(`[x-article] Copying image: ${path.basename(img.localPath)}`);
|
||||
|
||||
// Copy image to clipboard
|
||||
if (!copyImageToClipboard(img.localPath)) {
|
||||
@@ -535,17 +561,48 @@ export async function publishArticle(options: ArticleOptions): Promise<void> {
|
||||
}
|
||||
|
||||
// Wait for clipboard to be fully ready
|
||||
await sleep(800);
|
||||
await sleep(1000);
|
||||
|
||||
// Delete placeholder by pressing Enter (placeholder is already selected)
|
||||
console.log(`[x-article] Deleting placeholder with Enter...`);
|
||||
await cdp.send('Input.dispatchKeyEvent', { type: 'keyDown', key: 'Enter', code: 'Enter', windowsVirtualKeyCode: 13 }, { sessionId });
|
||||
await cdp.send('Input.dispatchKeyEvent', { type: 'keyUp', key: 'Enter', code: 'Enter', windowsVirtualKeyCode: 13 }, { sessionId });
|
||||
// Delete placeholder by pressing Backspace (more reliable than Enter for replacing selection)
|
||||
console.log(`[x-article] Deleting placeholder...`);
|
||||
await cdp.send('Input.dispatchKeyEvent', { type: 'keyDown', key: 'Backspace', code: 'Backspace', windowsVirtualKeyCode: 8 }, { sessionId });
|
||||
await cdp.send('Input.dispatchKeyEvent', { type: 'keyUp', key: 'Backspace', code: 'Backspace', windowsVirtualKeyCode: 8 }, { sessionId });
|
||||
|
||||
// Wait and verify placeholder is deleted
|
||||
await sleep(500);
|
||||
|
||||
// Check that placeholder is no longer in editor
|
||||
const afterDelete = await cdp.send<{ result: { value: boolean } }>('Runtime.evaluate', {
|
||||
expression: `(() => {
|
||||
const editor = document.querySelector('.DraftEditor-editorContainer [data-contents="true"]');
|
||||
if (!editor) return true;
|
||||
return !editor.innerText.includes(${JSON.stringify(img.placeholder)});
|
||||
})()`,
|
||||
returnByValue: true,
|
||||
}, { sessionId });
|
||||
|
||||
if (!afterDelete.result.value) {
|
||||
console.warn(`[x-article] Placeholder may not have been deleted, trying again...`);
|
||||
// Try selecting and deleting again
|
||||
await selectPlaceholder(1);
|
||||
await sleep(300);
|
||||
await cdp.send('Input.dispatchKeyEvent', { type: 'keyDown', key: 'Backspace', code: 'Backspace', windowsVirtualKeyCode: 8 }, { sessionId });
|
||||
await cdp.send('Input.dispatchKeyEvent', { type: 'keyUp', key: 'Backspace', code: 'Backspace', windowsVirtualKeyCode: 8 }, { sessionId });
|
||||
await sleep(500);
|
||||
}
|
||||
|
||||
// Focus editor to ensure cursor is in position
|
||||
await cdp.send('Runtime.evaluate', {
|
||||
expression: `(() => {
|
||||
const editor = document.querySelector('.DraftEditor-editorContainer [contenteditable="true"]');
|
||||
if (editor) editor.focus();
|
||||
})()`,
|
||||
}, { sessionId });
|
||||
await sleep(300);
|
||||
|
||||
// Paste image using paste script (activates Chrome, sends real keystroke)
|
||||
console.log(`[x-article] Pasting image...`);
|
||||
if (pasteFromClipboard('Google Chrome', 5, 800)) {
|
||||
if (pasteFromClipboard('Google Chrome', 5, 1000)) {
|
||||
console.log(`[x-article] Image pasted: ${path.basename(img.localPath)}`);
|
||||
} else {
|
||||
console.warn(`[x-article] Failed to paste image after retries`);
|
||||
|
||||
+254
-177
@@ -5,140 +5,149 @@ description: Generate professional slide deck images from content. Creates compr
|
||||
|
||||
# Slide Deck Generator
|
||||
|
||||
Transform content into professional slide deck with comprehensive outlines and generated slide images.
|
||||
Transform content into professional slide deck images with flexible style options.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
# From markdown file
|
||||
/baoyu-slide-deck path/to/article.md
|
||||
# Auto-select style based on content
|
||||
/baoyu-slide-deck path/to/content.md
|
||||
|
||||
# With style preference
|
||||
/baoyu-slide-deck path/to/article.md --style corporate
|
||||
/baoyu-slide-deck path/to/article.md --style playful
|
||||
/baoyu-slide-deck path/to/article.md --style technical
|
||||
# Specify a style
|
||||
/baoyu-slide-deck path/to/content.md --style sketch-notes
|
||||
/baoyu-slide-deck path/to/content.md --style minimal
|
||||
|
||||
# With audience specification
|
||||
/baoyu-slide-deck path/to/article.md --audience beginners
|
||||
/baoyu-slide-deck path/to/article.md --audience executives
|
||||
# Target specific audience
|
||||
/baoyu-slide-deck path/to/content.md --audience executives
|
||||
|
||||
# With language
|
||||
/baoyu-slide-deck path/to/article.md --lang zh
|
||||
/baoyu-slide-deck path/to/article.md --lang en
|
||||
# Set output language
|
||||
/baoyu-slide-deck path/to/content.md --lang zh
|
||||
|
||||
# Outline only (no image generation)
|
||||
/baoyu-slide-deck path/to/article.md --outline-only
|
||||
# Limit slide count
|
||||
/baoyu-slide-deck path/to/content.md --slides 10
|
||||
|
||||
# Generate outline only (skip image generation)
|
||||
/baoyu-slide-deck path/to/content.md --outline-only
|
||||
|
||||
# Direct content input
|
||||
/baoyu-slide-deck
|
||||
[paste content]
|
||||
|
||||
# Combine options
|
||||
/baoyu-slide-deck path/to/content.md --style storytelling --audience experts --slides 15
|
||||
```
|
||||
|
||||
## Options
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `--style <name>` | Visual style preset (see Style Gallery) |
|
||||
| `--audience <type>` | Target audience level |
|
||||
| `--lang <code>` | Output language (en, zh, etc.) |
|
||||
| `--slides <number>` | Target slide count (max 20) |
|
||||
| `--style <name>` | Visual style (see Style Gallery) |
|
||||
| `--audience <type>` | Target audience: beginners, intermediate, experts, executives, general |
|
||||
| `--lang <code>` | Output language for prompts and slides (en, zh, ja, etc.) |
|
||||
| `--slides <number>` | Target slide count |
|
||||
| `--outline-only` | Generate outline only, skip image generation |
|
||||
|
||||
## Style Gallery
|
||||
|
||||
| Style | Description |
|
||||
|-------|-------------|
|
||||
| `editorial` (Default) | Clean, sophisticated, minimalist |
|
||||
| `corporate` | Professional, trustworthy, polished |
|
||||
| `technical` | Precise, data-driven, analytical |
|
||||
| `playful` | Bold, energetic, engaging |
|
||||
| `minimal` | Ultra-clean, zen-like, focused |
|
||||
| `storytelling` | Narrative-driven, cinematic, immersive |
|
||||
| `warm` | Cozy, healing, hand-drawn illustration style |
|
||||
| `retro-flat` | Flat vector illustration with retro palette |
|
||||
| `notion` | Minimalist hand-drawn line art, intellectual |
|
||||
| Style | Description | Best For |
|
||||
|-------|-------------|----------|
|
||||
| `sketch-notes` | Hand-drawn sketch notes, warm & friendly | Educational, tutorials, knowledge sharing |
|
||||
| `blueprint` | Technical blueprint, precise & analytical | Architecture, system design, data analysis |
|
||||
| `bold-editorial` | Magazine editorial, high-impact & dynamic | Product launches, marketing, keynotes |
|
||||
| `vector-illustration` | Flat vector with black outlines, retro & cute | Creative proposals, children's content, brand showcases |
|
||||
| `minimal` | Ultra-clean, maximum whitespace | Executive briefings, keynotes, premium brands |
|
||||
| `storytelling` | Cinematic, full-bleed visuals | Narratives, case studies, emotional impact |
|
||||
| `warm` | Soft gradients, wellness aesthetic | Lifestyle, wellness, personal development |
|
||||
| `notion` (Default) | SaaS dashboard, clean data focus | Product demos, SaaS, productivity tools |
|
||||
| `corporate` | Navy/gold, professional business | Investor decks, client proposals, quarterly reports |
|
||||
| `playful` | Vibrant colors, dynamic rounded shapes | Workshops, training, creative pitches |
|
||||
|
||||
Detailed style definitions: `references/styles/<style>.md`
|
||||
|
||||
## Auto Style Selection
|
||||
|
||||
When no `--style` is specified, analyze content for style signals:
|
||||
|
||||
| Content Signals | Selected Style |
|
||||
|----------------|----------------|
|
||||
| AI, coding, tech, digital, algorithm, data | `technical` |
|
||||
| Business, strategy, investment, corporate | `corporate` |
|
||||
| Personal story, journey, narrative, emotion | `storytelling` |
|
||||
| Simple, zen, focus, essential, one idea | `minimal` |
|
||||
| Fun, creative, workshop, educational | `playful` |
|
||||
| Research, analysis, thought leadership | `editorial` |
|
||||
| Wellness, healing, cozy, self-care, lifestyle, comfort | `warm` |
|
||||
| Tutorial, explainer, how-to, beginner, product, guide | `retro-flat` |
|
||||
| Knowledge, concept, productivity, SaaS, notion, intellectual | `notion` |
|
||||
|-----------------|----------------|
|
||||
| tutorial, learn, education, guide, intro, beginner | `sketch-notes` |
|
||||
| architecture, system, data, analysis, technical, engineering | `blueprint` |
|
||||
| launch, marketing, brand, keynote, impact, showcase | `bold-editorial` |
|
||||
| creative, children, kids, cute, illustration, retro | `vector-illustration` |
|
||||
| executive, minimal, clean, simple, elegant | `minimal` |
|
||||
| story, journey, case study, narrative, emotional | `storytelling` |
|
||||
| wellness, lifestyle, personal, growth, mindfulness | `warm` |
|
||||
| saas, product, dashboard, metrics, productivity | `notion` |
|
||||
| investor, quarterly, business, corporate, proposal, client | `corporate` |
|
||||
| workshop, training, fun, playful, energetic, team | `playful` |
|
||||
| Default | `notion` |
|
||||
|
||||
## Audience Presets
|
||||
## Design Philosophy
|
||||
|
||||
| Audience | Approach |
|
||||
|----------|----------|
|
||||
| `beginners` | Step-by-step, more context, simpler visuals |
|
||||
| `intermediate` | Balanced detail, some assumed knowledge |
|
||||
| `experts` | Dense information, technical depth, less hand-holding |
|
||||
| `executives` | High-level insights, key metrics, strategic focus |
|
||||
| `general` | Accessible language, broad appeal, clear takeaways |
|
||||
This deck is designed for **reading and sharing**, not live presentation:
|
||||
- Each slide must be **self-explanatory** without verbal commentary
|
||||
- Structure content for **logical flow** when scrolling
|
||||
- Include **all necessary context** within each slide
|
||||
- Optimize for **social media sharing** and offline reading
|
||||
|
||||
## File Management
|
||||
|
||||
### With Article Path
|
||||
### With Content Path
|
||||
|
||||
Save to `slide-deck/` subdirectory in the same folder as the content:
|
||||
|
||||
```
|
||||
path/to/
|
||||
├── article.md
|
||||
content-dir/
|
||||
├── source-content.md
|
||||
└── slide-deck/
|
||||
├── outline.md
|
||||
├── prompts/
|
||||
│ ├── 01-cover.md
|
||||
│ ├── 02-content-1.md
|
||||
│ ├── 01-slide-cover.md
|
||||
│ ├── 02-slide-{slug}.md
|
||||
│ └── ...
|
||||
├── 01-cover.png
|
||||
├── 02-content-1.png
|
||||
├── 01-slide-cover.png
|
||||
├── 02-slide-{slug}.png
|
||||
└── ...
|
||||
```
|
||||
|
||||
### Without Article Path
|
||||
### Without Content Path
|
||||
|
||||
Save to `slide-outputs/YYYY-MM-DD/[topic-slug]/`:
|
||||
|
||||
```
|
||||
./baoyu-slide-deck-outputs/YYYY-MM-DD/[topic-slug]/
|
||||
├── outline.md
|
||||
├── prompts/
|
||||
│ ├── 01-cover.md
|
||||
│ └── ...
|
||||
├── 01-cover.png
|
||||
└── ...
|
||||
slide-outputs/
|
||||
└── 2026-01-17/
|
||||
└── ai-future-trends/
|
||||
├── outline.md
|
||||
├── prompts/
|
||||
│ ├── 01-slide-cover.md
|
||||
│ └── ...
|
||||
├── 01-slide-cover.png
|
||||
└── ...
|
||||
```
|
||||
|
||||
## Workflow
|
||||
|
||||
### Step 1: Analyze Content & Select Style
|
||||
### Step 1: Analyze Content & Determine Settings
|
||||
|
||||
1. Read source content
|
||||
2. If `--style` specified, use that style
|
||||
3. Otherwise, analyze content for style signals
|
||||
4. Extract key information:
|
||||
- Core narrative and key messages
|
||||
- Important data points and statistics
|
||||
- Logical flow and structure
|
||||
- Target audience signals
|
||||
1. Read source material
|
||||
2. **Style selection**:
|
||||
- If `--style` specified, use that style
|
||||
- Otherwise, scan for style signals and auto-select
|
||||
3. **Language detection**:
|
||||
- If `--lang` specified, use that language for all prompts and slide text
|
||||
- Otherwise, detect language from source material
|
||||
- If uncertain (mixed languages or unclear), ask user to confirm
|
||||
4. **Slide count**:
|
||||
- If `--slides` specified, use that count
|
||||
- Otherwise, dynamic based on content structure
|
||||
|
||||
### Step 2: Generate Outline
|
||||
### Step 2: Generate Outline with Style Instructions
|
||||
|
||||
Create outline with `STYLE_INSTRUCTIONS` block and slide specifications.
|
||||
|
||||
**Outline Format**:
|
||||
Create outline with structured STYLE_INSTRUCTIONS block:
|
||||
|
||||
```markdown
|
||||
# Slide Deck Outline: [Topic]
|
||||
# Slide Deck Outline
|
||||
|
||||
**Source**: [source file or "Direct input"]
|
||||
**Topic**: [topic description]
|
||||
**Style**: [selected style]
|
||||
**Audience**: [target audience]
|
||||
**Language**: [output language]
|
||||
@@ -148,152 +157,220 @@ Create outline with `STYLE_INSTRUCTIONS` block and slide specifications.
|
||||
---
|
||||
|
||||
<STYLE_INSTRUCTIONS>
|
||||
Design Aesthetic: [Overall style description]
|
||||
Background Color: [Description and Hex Code]
|
||||
Primary Font: [Font name for Headlines]
|
||||
Secondary Font: [Font name for Body copy]
|
||||
Design Aesthetic: [2-3 sentence description from style file]
|
||||
|
||||
Background:
|
||||
Color: [Name] ([Hex])
|
||||
Texture: [description]
|
||||
|
||||
Typography:
|
||||
Primary Font: [detailed description for image generation]
|
||||
Secondary Font: [detailed description for image generation]
|
||||
|
||||
Color Palette:
|
||||
Primary Text Color: [Hex Code]
|
||||
Primary Accent Color: [Hex Code]
|
||||
Visual Elements: [Lines, shapes, imagery style, etc.]
|
||||
Primary Text: [Name] ([Hex]) - [usage]
|
||||
Background: [Name] ([Hex]) - [usage]
|
||||
Accent 1: [Name] ([Hex]) - [usage]
|
||||
Accent 2: [Name] ([Hex]) - [usage]
|
||||
|
||||
Visual Elements:
|
||||
- [element 1 with rendering guidance]
|
||||
- [element 2 with rendering guidance]
|
||||
- ...
|
||||
|
||||
Style Rules:
|
||||
Do: [guidelines from style file]
|
||||
Don't: [anti-patterns from style file]
|
||||
</STYLE_INSTRUCTIONS>
|
||||
|
||||
---
|
||||
|
||||
## Slide 1: [Descriptive Title]
|
||||
## Slide 1 of N
|
||||
|
||||
**Position**: Cover
|
||||
**Filename**: 01-cover.png
|
||||
**Type**: Cover
|
||||
**Filename**: 01-slide-cover.png
|
||||
|
||||
// NARRATIVE GOAL
|
||||
[Storytelling purpose within the overall arc]
|
||||
[What this slide achieves in the story arc]
|
||||
|
||||
// KEY CONTENT
|
||||
Headline: [Main message - narrative, not "Title: Subtitle" format]
|
||||
Sub-headline: [Supporting context]
|
||||
Body:
|
||||
- [Key point 1 with specific data from source]
|
||||
- [Key point 2 with specific data from source]
|
||||
Headline: [main title]
|
||||
Sub-headline: [supporting tagline]
|
||||
|
||||
// VISUAL
|
||||
[Detailed description of imagery, charts, graphics, or abstract visuals]
|
||||
[Detailed visual description - specific elements, composition, mood]
|
||||
|
||||
// LAYOUT
|
||||
[Composition, hierarchy, spatial arrangement, focus points]
|
||||
[Composition, hierarchy, spatial arrangement]
|
||||
|
||||
---
|
||||
|
||||
## Slide 2: [First Content]
|
||||
## Slide 2 of N
|
||||
|
||||
**Type**: Content
|
||||
**Filename**: 02-slide-{slug}.png
|
||||
|
||||
// NARRATIVE GOAL
|
||||
[What this slide achieves in the story arc]
|
||||
|
||||
// KEY CONTENT
|
||||
Headline: [main message - narrative, not label]
|
||||
Sub-headline: [supporting context]
|
||||
Body:
|
||||
- [point 1 with specific detail]
|
||||
- [point 2 with specific detail]
|
||||
- [point 3 with specific detail]
|
||||
|
||||
// VISUAL
|
||||
[Detailed visual description]
|
||||
|
||||
// LAYOUT
|
||||
[Composition, hierarchy, spatial arrangement]
|
||||
|
||||
---
|
||||
...
|
||||
|
||||
## Slide N: [Back Cover]
|
||||
...
|
||||
## Slide N of N
|
||||
|
||||
**Type**: Back Cover
|
||||
**Filename**: {NN}-slide-back-cover.png
|
||||
|
||||
// NARRATIVE GOAL
|
||||
[Meaningful closing - not just "thank you"]
|
||||
|
||||
// KEY CONTENT
|
||||
Headline: [memorable closing statement or call-to-action]
|
||||
Body: [optional summary points or next steps]
|
||||
|
||||
// VISUAL
|
||||
[Visual that reinforces the core message]
|
||||
|
||||
// LAYOUT
|
||||
[Clean, impactful composition]
|
||||
```
|
||||
|
||||
**Required Slide Structure**:
|
||||
1. **Slide 1**: Cover Slide (poster-style, heroic typography)
|
||||
2. **Slides 2-N-1**: Content slides (consistent internal style)
|
||||
3. **Slide N**: Back Cover (closing statement, not "Thank You")
|
||||
|
||||
### Step 3: Save Outline
|
||||
|
||||
Save outline as `outline.md` in target directory.
|
||||
Save outline as `outline.md` in the output directory.
|
||||
|
||||
If `--outline-only` flag is set, stop here.
|
||||
If `--outline-only` is specified, stop here.
|
||||
|
||||
### Step 4: Create Prompt Files
|
||||
### Step 4: Generate Prompts
|
||||
|
||||
For each slide, create a style-specific prompt file.
|
||||
Create prompt file per slide in `prompts/` directory:
|
||||
|
||||
**Prompt Format**:
|
||||
|
||||
```markdown
|
||||
Slide theme: [slide title]
|
||||
Style: [style name]
|
||||
Position: [cover/content/back-cover]
|
||||
|
||||
Visual composition:
|
||||
- Main visual: [style-appropriate description from VISUAL section]
|
||||
- Layout: [from LAYOUT section]
|
||||
- Decorative elements: [style-specific decorations]
|
||||
|
||||
Color scheme:
|
||||
- Background: [style background color]
|
||||
- Primary text: [style text color]
|
||||
- Accent: [style accent color]
|
||||
|
||||
Text content:
|
||||
- Headline: [headline text]
|
||||
- Sub-headline: [sub-headline if any]
|
||||
- Body points: [bullet points if any]
|
||||
|
||||
Style notes: [specific style characteristics to emphasize]
|
||||
```
|
||||
1. Read `references/base-prompt.md`
|
||||
2. Combine with style-specific instructions from outline
|
||||
3. Add slide-specific content from outline
|
||||
4. Save as `01-slide-cover.md`, `02-slide-{slug}.md`, etc.
|
||||
|
||||
### Step 5: Generate Images
|
||||
|
||||
For each slide, generate using:
|
||||
**Image Generation Skill Selection**:
|
||||
1. Check available image generation skills
|
||||
2. If multiple skills available, ask user to choose
|
||||
|
||||
**Session Management**:
|
||||
If the image generation skill supports `--sessionId`:
|
||||
1. Generate a unique session ID at the start (e.g., `slides-{topic-slug}-{timestamp}`)
|
||||
2. Use the same session ID for all slides in the series
|
||||
3. This ensures style consistency across all generated slides
|
||||
|
||||
**Generation Flow**:
|
||||
1. Call selected image generation skill with prompt file, output path, and session ID
|
||||
2. Confirm generation success
|
||||
3. Report progress: "Generated X/N"
|
||||
4. Continue to next
|
||||
|
||||
### Step 6: Merge to PPTX
|
||||
|
||||
After all images are generated, merge them into a PowerPoint file:
|
||||
|
||||
```bash
|
||||
/baoyu-gemini-web --promptfiles [SKILL_ROOT]/skills/baoyu-slide-deck/prompts/system.md [TARGET_DIR]/prompts/01-cover.md --image [TARGET_DIR]/01-cover.png
|
||||
npx -y bun skills/baoyu-slide-deck/scripts/merge-to-pptx.ts <slide-deck-dir>
|
||||
```
|
||||
|
||||
Generation flow:
|
||||
1. Generate images sequentially
|
||||
2. After each image, output progress: "Generated X/N"
|
||||
3. On failure, auto-retry once
|
||||
4. If retry fails, log reason, continue to next
|
||||
This creates `{topic-slug}.pptx` in the slide deck directory with:
|
||||
- All images as full-bleed 16:9 slides
|
||||
- Prompt content added as speaker notes (from `prompts/` directory)
|
||||
|
||||
### Step 6: Completion Report
|
||||
### Step 7: Output Summary
|
||||
|
||||
```
|
||||
Slide Deck Generated!
|
||||
Slide Deck Complete!
|
||||
|
||||
Topic: [topic]
|
||||
Style: [style name]
|
||||
Audience: [audience]
|
||||
Audience: [audience type]
|
||||
Location: [directory path]
|
||||
Slides: N total
|
||||
|
||||
- 01-cover.png ✓ Cover
|
||||
- 02-content-1.png ✓ Content
|
||||
- 03-content-2.png ✓ Content
|
||||
- 01-slide-cover.png ✓ Cover
|
||||
- 02-slide-intro.png ✓ Content
|
||||
- 03-slide-main-point.png ✓ Content
|
||||
- ...
|
||||
- 0N-back-cover.png ✓ Back Cover
|
||||
- {NN}-slide-back-cover.png ✓ Back Cover
|
||||
|
||||
Outline: outline.md
|
||||
|
||||
[If any failures]
|
||||
Failed:
|
||||
- 0X-slide-name.png: [failure reason]
|
||||
PPTX: {topic-slug}.pptx
|
||||
```
|
||||
|
||||
## Content Rules
|
||||
|
||||
1. **Respect reader attention** -
|
||||
2. **Data traceability** - All statistics must include source attribution
|
||||
3. **Self-contained prompts** - Every detail in the image prompt, no external references
|
||||
4. **No placeholders** - Every element must be fully specified
|
||||
|
||||
## Style Rules
|
||||
|
||||
1. **Narrative headlines** - Headlines tell the story, not label the content
|
||||
- Bad: "Key Statistics"
|
||||
- Good: "Usage doubled in 6 months"
|
||||
|
||||
2. **Avoid AI clichés** - No "dive into", "explore", "journey", "let's"
|
||||
|
||||
3. **Meaningful back cover** - Not just "Thank you"
|
||||
- Include call-to-action, key takeaway, or memorable closing
|
||||
|
||||
4. **Consistent visual language** - Same icons, colors, layouts throughout
|
||||
|
||||
## Slide Structure
|
||||
|
||||
1. **Cover (Slide 1)**: Title, visual hook, topic introduction
|
||||
2. **Content (Middle)**: Key points, data, explanations - dynamic count based on content
|
||||
3. **Back Cover (Final)**: Summary, call-to-action, or memorable closing
|
||||
|
||||
## Key Specifications
|
||||
|
||||
- **Aspect Ratio**: 16:9 (landscape)
|
||||
- **Slide Count**: Dynamic based on content
|
||||
- **Required Slides**: Cover + Back Cover minimum
|
||||
- **No slide numbers, footers, or logos**
|
||||
- **Language**: Priority order: `--lang` option → source material language → ask user if uncertain
|
||||
- **Tone**: Direct, confident language (avoid AI-sounding phrases)
|
||||
|
||||
## Style Reference Details
|
||||
|
||||
| Style | Description |
|
||||
|-------|-------------|
|
||||
| `sketch-notes` | Hand-drawn feel, soft brush strokes, warm off-white background, conceptual icons |
|
||||
| `blueprint` | Technical schematics, grid texture, precise lines, engineering blue tones |
|
||||
| `bold-editorial` | High contrast, bold typography, dark backgrounds, magazine-level impact |
|
||||
| `vector-illustration` | Flat vector, black outlines, retro colors, toy model aesthetic |
|
||||
| `minimal` | Maximum whitespace, single accent color, clean sans-serif, zen-like |
|
||||
| `storytelling` | Full-bleed imagery, cinematic compositions, emotional photography |
|
||||
| `warm` | Soft gradients, rounded shapes, wellness palette, approachable |
|
||||
| `notion` | Dashboard aesthetic, clean data viz, SaaS-inspired, productivity focus |
|
||||
| `corporate` | Navy/gold palette, structured layouts, professional iconography, business polish |
|
||||
| `playful` | Vibrant coral/teal/yellow, rounded shapes, dynamic layouts, energetic |
|
||||
|
||||
Full style specifications: `references/styles/<style>.md`
|
||||
|
||||
## Notes
|
||||
|
||||
### Design Philosophy
|
||||
- Deck is designed for **reading and sharing**, not live presentation
|
||||
- Structure should be self-explanatory without a presenter
|
||||
- Include enough context for visuals to be understood standalone
|
||||
- Err on the side of audience having **more expertise** than expected
|
||||
|
||||
### Content Rules
|
||||
- Maximum 20 slides per deck
|
||||
- Every data point must trace to source material
|
||||
- All details in prompts - image generator has no access to source
|
||||
|
||||
### Style Rules
|
||||
- Avoid AI-generated clichés ("It wasn't just X, it was Y")
|
||||
- Use narrative headlines, not "Title: Subtitle" format
|
||||
- Cover and Back Cover should be visually distinct (poster-style)
|
||||
- Back Cover should be meaningful closure, not "Thank You" or "Questions?"
|
||||
|
||||
### Prohibited
|
||||
- Never include photorealistic images of prominent individuals
|
||||
- Never include placeholder slides for author name, date, etc.
|
||||
|
||||
### Image Generation
|
||||
- Image generation typically takes 10-30 seconds per slide
|
||||
- Auto-retry once on generation failure
|
||||
- Use cartoon alternatives for sensitive public figures
|
||||
- Output language matches content language
|
||||
- Maintain style consistency across all slides
|
||||
- Use stylized alternatives for sensitive public figures
|
||||
- Output language matches input content language (or `--lang`)
|
||||
- Maintain style consistency across all slides in deck
|
||||
|
||||
@@ -3,38 +3,57 @@ Create a presentation slide image following these guidelines:
|
||||
## Image Specifications
|
||||
|
||||
- **Type**: Presentation slide
|
||||
- **Orientation**: Landscape (horizontal)
|
||||
- **Aspect Ratio**: 16:9
|
||||
- **Style**: Hand-drawn illustration with professional typography
|
||||
- **Aspect Ratio**: 16:9 (landscape)
|
||||
- **Style**: Professional slide deck
|
||||
|
||||
## Core Persona: The Architect
|
||||
|
||||
You are "The Architect" - a master visual storyteller creating presentation slides. Your slides:
|
||||
- Tell a visual story that complements the narrative
|
||||
- Use bold, confident visual language
|
||||
- Balance information density with visual clarity
|
||||
- Create memorable, impactful visuals
|
||||
|
||||
## Core Principles
|
||||
|
||||
- Hand-drawn quality throughout - NO realistic or photographic elements
|
||||
- If content involves sensitive or copyrighted figures, create stylistically similar alternatives - DO NOT refuse to generate
|
||||
- Clear visual hierarchy: headline dominates, supporting content secondary
|
||||
- Ample whitespace, avoid cluttered layouts
|
||||
- Professional presentation aesthetic
|
||||
- NO slide numbers, page numbers, footers, headers, or logos
|
||||
- Clean, uncluttered layouts with clear visual hierarchy
|
||||
- Each slide conveys ONE clear message
|
||||
|
||||
## Text Style (CRITICAL)
|
||||
|
||||
- **ALL text MUST be hand-drawn style**
|
||||
- Headlines: Large, bold, prominent
|
||||
- Body text: Clean, readable, well-spaced
|
||||
- Use visual emphasis for key terms (color, size, underline)
|
||||
- **DO NOT use realistic or computer-generated fonts**
|
||||
- **ALL text MUST match the designated style exactly**
|
||||
- Title text: Large, bold, immediately readable
|
||||
- Body text: Clear, legible, appropriate sizing
|
||||
- Max 3-4 text elements per slide
|
||||
- **DO NOT use realistic or computer-generated fonts unless style specifies**
|
||||
- **Font rendering must match the style aesthetic** (hand-drawn for sketch styles, clean for minimal styles)
|
||||
|
||||
## Layout Principles
|
||||
|
||||
- Consistent margins and spacing
|
||||
- Clear information hierarchy
|
||||
- Visual elements support but don't overwhelm text
|
||||
- Balance between text and imagery
|
||||
- **Visual Hierarchy**: Most important element gets most visual weight
|
||||
- **Breathing Room**: Generous margins and spacing between elements
|
||||
- **Alignment**: Consistent alignment creates professional feel
|
||||
- **Balance**: Distribute visual weight evenly (symmetrical or asymmetrical)
|
||||
- **Focal Point**: One clear area draws the eye first
|
||||
- **Rule of Thirds**: Key elements at intersection points for dynamic compositions
|
||||
- **Z-Pattern**: For text-heavy slides, arrange content in natural reading flow
|
||||
|
||||
## Language
|
||||
|
||||
- Use the same language as the content provided below
|
||||
- Use the same language as the content provided below for all text elements
|
||||
- Match punctuation style to the content language
|
||||
- Write in direct, confident language
|
||||
- Avoid AI-sounding phrases like "dive into", "explore", "let's", "journey"
|
||||
|
||||
---
|
||||
|
||||
Please use nano banana pro to generate the slide based on the content provided below:
|
||||
## STYLE_INSTRUCTIONS
|
||||
|
||||
[Insert style-specific instructions here]
|
||||
|
||||
---
|
||||
|
||||
Please use nano banana pro to generate the slide image based on the content provided below:
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
# blueprint
|
||||
|
||||
Precise technical blueprint style with professional analytical visual presentation
|
||||
|
||||
## Design Aesthetic
|
||||
|
||||
Clean, structured visual metaphors using blueprints, diagrams, and schematics. Precise, analytical and aesthetically refined. Information presented in triptych or grid-based layouts with engineering precision.
|
||||
|
||||
## Background
|
||||
|
||||
- Color: Blueprint Off-White (#FAF8F5)
|
||||
- Texture: Subtle grid overlay, light engineering paper feel
|
||||
|
||||
## Typography
|
||||
|
||||
### Primary Font (Headlines)
|
||||
|
||||
Neue Haas Grotesk Display Pro or similar clean sans-serif. Bold weight for titles. Precise letterforms with consistent spacing. Technical, authoritative presence.
|
||||
|
||||
### Secondary Font (Body)
|
||||
|
||||
Tiempos Text or similar elegant serif for body explanations. Clean, readable at smaller sizes. Professional editorial quality.
|
||||
|
||||
## Color Palette
|
||||
|
||||
| Role | Color | Hex | Usage |
|
||||
|------|-------|-----|-------|
|
||||
| Background | Blueprint Paper | #FAF8F5 | Primary background |
|
||||
| Grid | Light Gray | #E5E5E5 | Background grid lines |
|
||||
| Primary Text | Deep Slate | #334155 | Headlines, body text |
|
||||
| Primary Accent | Engineering Blue | #2563EB | Key elements, highlights |
|
||||
| Secondary Accent | Navy Blue | #1E3A5F | Supporting elements |
|
||||
| Tertiary | Light Blue | #BFDBFE | Backgrounds, fills |
|
||||
| Warning | Amber | #F59E0B | Warnings, emphasis points |
|
||||
|
||||
## Visual Elements
|
||||
|
||||
- Precise lines with consistent stroke weights
|
||||
- Technical schematics and clean vector graphics
|
||||
- Thin line work in technical drawing style
|
||||
- Connection lines use straight lines or 90-degree angles only
|
||||
- Data visualization with clean, minimal charts
|
||||
- Dimension lines and measurement indicators
|
||||
- Cross-section style diagrams
|
||||
- Isometric or orthographic projections
|
||||
|
||||
## Style Rules
|
||||
|
||||
### Do
|
||||
|
||||
- Maintain consistent line weights throughout
|
||||
- Use grid alignment for all elements
|
||||
- Keep color palette restrained and unified
|
||||
- Create clear visual hierarchy through scale
|
||||
- Use geometric precision for all shapes
|
||||
|
||||
### Don't
|
||||
|
||||
- Use hand-drawn or organic shapes
|
||||
- Add decorative flourishes
|
||||
- Use curved connection lines
|
||||
- Include photographic elements
|
||||
- Add slide numbers, footers, or logos
|
||||
|
||||
## Best For
|
||||
|
||||
Technical architecture, system design, data analysis, professional business presentations, engineering documentation, process flows
|
||||
@@ -0,0 +1,69 @@
|
||||
# bold-editorial
|
||||
|
||||
High-impact magazine editorial style with bold visual expression
|
||||
|
||||
## Design Aesthetic
|
||||
|
||||
Strong visual impact at magazine cover level. Bold typography and dramatic contrast. Full-bleed imagery and large color blocks create commanding presence. Every slide feels like a premium publication cover.
|
||||
|
||||
## Background
|
||||
|
||||
- Color: Deep Black (#0A0A0A) primary, or Deep Blue (#0F172A) alternative
|
||||
- Texture: None - clean solid backgrounds, or pure white with bold color blocks
|
||||
|
||||
## Typography
|
||||
|
||||
### Primary Font (Headlines)
|
||||
|
||||
Bold condensed typeface like Impact, Oswald Bold, or Bebas Neue. Oversized headlines that dominate the slide. All-caps for maximum impact. Tight letter-spacing.
|
||||
|
||||
### Secondary Font (Body)
|
||||
|
||||
Clean sans-serif such as Inter, SF Pro, or Helvetica Neue. Medium weight for body text. High contrast against background.
|
||||
|
||||
## Color Palette
|
||||
|
||||
| Role | Color | Hex | Usage |
|
||||
|------|-------|-----|-------|
|
||||
| Background Dark | Deep Black | #0A0A0A | Primary dark background |
|
||||
| Background Alt | Deep Blue | #0F172A | Alternative dark background |
|
||||
| Background Light | Pure White | #FFFFFF | Light mode background |
|
||||
| Primary Text | Pure White | #FFFFFF | Text on dark backgrounds |
|
||||
| Alt Text | Pure Black | #000000 | Text on light backgrounds |
|
||||
| Accent 1 | Electric Blue | #3B82F6 | Primary highlights |
|
||||
| Accent 2 | Bright Orange | #FB923C | Energy, urgency |
|
||||
| Accent 3 | Magenta | #EC4899 | Creative, bold accents |
|
||||
| Accent 4 | Neon Green | #22C55E | Success, growth |
|
||||
| Accent 5 | Violet | #8B5CF6 | Innovation, premium |
|
||||
|
||||
## Visual Elements
|
||||
|
||||
- Strong typography as visual element itself
|
||||
- Geometric shapes and bold color blocks
|
||||
- Full-bleed images or solid color backgrounds
|
||||
- High contrast gradients (subtle, not garish)
|
||||
- Minimal decoration - let content speak
|
||||
- Dynamic diagonal lines and angles
|
||||
- Dramatic lighting effects on text
|
||||
|
||||
## Style Rules
|
||||
|
||||
### Do
|
||||
|
||||
- Use extreme scale contrast (huge headlines, small body)
|
||||
- Create bold color block compositions
|
||||
- Let negative space create tension
|
||||
- Use full-bleed backgrounds
|
||||
- Make every slide feel like a magazine cover
|
||||
|
||||
### Don't
|
||||
|
||||
- Use soft or muted colors
|
||||
- Add unnecessary decorative elements
|
||||
- Create busy, cluttered layouts
|
||||
- Use thin or delicate typography
|
||||
- Add slide numbers, footers, or logos
|
||||
|
||||
## Best For
|
||||
|
||||
Product launches, marketing presentations, keynote speeches, brand showcases, investor pitches, high-stakes presentations
|
||||
@@ -1,29 +1,69 @@
|
||||
# corporate
|
||||
|
||||
Professional, trustworthy, polished
|
||||
Professional business style with navy/gold palette and structured layouts
|
||||
|
||||
## Design Aesthetic
|
||||
|
||||
Clean lines, structured layouts, business-appropriate
|
||||
Clean lines, structured layouts, and business-appropriate sophistication. Projects competence, reliability, and institutional credibility. Balances professionalism with approachability through careful use of whitespace and refined color choices.
|
||||
|
||||
## Color Palette
|
||||
## Background
|
||||
|
||||
- Background: Clean white (#FFFFFF) with navy accents
|
||||
- Primary Text: Navy (#1E3A5F)
|
||||
- Accent: Gold (#C9A227)
|
||||
- Color: Pure White (#FFFFFF) with navy structural elements
|
||||
- Texture: None - crisp digital clarity for maximum professionalism
|
||||
|
||||
## Typography
|
||||
|
||||
- Primary Font: Inter (headlines)
|
||||
- Secondary Font: Source Sans Pro (body)
|
||||
### Primary Font (Headlines)
|
||||
|
||||
Modern geometric sans-serif (Inter, SF Pro, or similar). Clean, professional, and highly legible. Conveys competence and contemporary business sensibility. Medium to semi-bold weight.
|
||||
|
||||
### Secondary Font (Body)
|
||||
|
||||
Humanist sans-serif (Source Sans Pro style) for body text. Friendly yet professional, optimized for reading comprehension. Regular weight with comfortable line height.
|
||||
|
||||
## Color Palette
|
||||
|
||||
| Role | Color | Hex | Usage |
|
||||
|------|-------|-----|-------|
|
||||
| Background | Pure White | #FFFFFF | Main slide background |
|
||||
| Primary Text | Navy | #1E3A5F | Headlines, key text |
|
||||
| Secondary Text | Dark Gray | #4A5568 | Body text |
|
||||
| Primary Accent | Gold | #C9A227 | Premium highlights, emphasis |
|
||||
| Secondary Accent | Light Navy | #3D5A80 | Secondary elements |
|
||||
| Success | Corporate Green | #059669 | Positive metrics |
|
||||
| Alert | Corporate Red | #DC2626 | Attention items |
|
||||
| Neutral | Light Gray | #F3F4F6 | Background sections |
|
||||
|
||||
## Visual Elements
|
||||
|
||||
- Clean charts
|
||||
- Professional icons
|
||||
- Structured grids
|
||||
- Subtle shadows
|
||||
- Clean charts and data visualizations
|
||||
- Professional iconography (outlined style)
|
||||
- Structured grid layouts
|
||||
- Subtle shadows for depth (minimal)
|
||||
- Progress bars and metrics displays
|
||||
- Organizational charts
|
||||
- Timeline graphics
|
||||
- Comparison tables
|
||||
|
||||
## Style Rules
|
||||
|
||||
### Do
|
||||
|
||||
- Maintain clear visual hierarchy
|
||||
- Use consistent grid alignment
|
||||
- Apply accent colors strategically (gold for emphasis)
|
||||
- Keep data visualizations clean and readable
|
||||
- Use professional outlined iconography
|
||||
|
||||
### Don't
|
||||
|
||||
- Use playful or casual elements
|
||||
- Apply heavy decorative effects
|
||||
- Mix too many accent colors
|
||||
- Crowd slides with information
|
||||
- Use informal illustration styles
|
||||
- Add slide numbers, footers, or logos
|
||||
|
||||
## Best For
|
||||
|
||||
Business presentations, investor decks, reports
|
||||
Business presentations, investor decks, quarterly reports, executive summaries, client proposals, corporate communications, board meetings
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
# editorial
|
||||
|
||||
Clean, sophisticated, minimalist
|
||||
|
||||
## Design Aesthetic
|
||||
|
||||
High-end journal, architectural blueprints
|
||||
|
||||
## Color Palette
|
||||
|
||||
- Background: Subtle textured off-white (#F8F7F5)
|
||||
- Primary Text: Dark slate grey (#2F3542)
|
||||
- Accent: Intelligent blue (#007AFF)
|
||||
|
||||
## Typography
|
||||
|
||||
- Primary Font: Neue Haas Grotesk Display Pro (bold headlines)
|
||||
- Secondary Font: Tiempos Text (body copy, annotations)
|
||||
|
||||
## Visual Elements
|
||||
|
||||
- Thin precise line work
|
||||
- Schematic diagrams
|
||||
- Clean vectors
|
||||
- Spacious layouts
|
||||
|
||||
## Best For
|
||||
|
||||
Thought leadership, research presentations
|
||||
@@ -1,29 +1,64 @@
|
||||
# minimal
|
||||
|
||||
Ultra-clean, zen-like, focused
|
||||
Ultra-clean keynote style with maximum whitespace and zen-like simplicity
|
||||
|
||||
## Design Aesthetic
|
||||
|
||||
Maximum whitespace, single focal points, elegant typography
|
||||
Maximum whitespace with minimal elements. Zen-like simplicity where every element earns its place. Premium, refined aesthetic suitable for executive audiences. Less is more - remove until nothing more can be taken away.
|
||||
|
||||
## Color Palette
|
||||
## Background
|
||||
|
||||
- Background: Pure white (#FFFFFF)
|
||||
- Primary Text: Pure black (#000000)
|
||||
- Accent: Single accent (content-derived)
|
||||
- Color: Pure White (#FFFFFF)
|
||||
- Texture: None - absolute clean, no grain or patterns
|
||||
|
||||
## Typography
|
||||
|
||||
- Primary Font: Helvetica Neue (headlines)
|
||||
- Secondary Font: Garamond (body)
|
||||
### Primary Font (Headlines)
|
||||
|
||||
Clean geometric sans-serif like SF Pro Display, Inter, or Helvetica Neue. Light to medium weight for elegant restraint. Generous letter-spacing. Large scale for impact without boldness.
|
||||
|
||||
### Secondary Font (Body)
|
||||
|
||||
Same family as headlines in lighter weight. Minimal size contrast. Clean, airy feeling throughout.
|
||||
|
||||
## Color Palette
|
||||
|
||||
| Role | Color | Hex | Usage |
|
||||
|------|-------|-----|-------|
|
||||
| Background | Pure White | #FFFFFF | Primary background |
|
||||
| Primary Text | Near Black | #1A1A1A | Headlines, body |
|
||||
| Secondary Text | Medium Gray | #6B7280 | Captions, metadata |
|
||||
| Accent | Single Brand Color | #2563EB | One accent only, sparingly |
|
||||
| Dividers | Light Gray | #E5E7EB | Subtle separators |
|
||||
|
||||
## Visual Elements
|
||||
|
||||
- Maximum whitespace
|
||||
- Single focal points
|
||||
- Hairline rules
|
||||
- Elegant typography
|
||||
- Single accent color used sparingly
|
||||
- Thin hairline rules for separation
|
||||
- Generous margins (minimum 15% on all sides)
|
||||
- Center or left-aligned layouts
|
||||
- Simple geometric shapes only when necessary
|
||||
- No decorative elements
|
||||
- Data visualizations in single color or grayscale
|
||||
|
||||
## Style Rules
|
||||
|
||||
### Do
|
||||
|
||||
- Embrace empty space as a design element
|
||||
- Use single accent color only
|
||||
- Keep text minimal (10 words or less per slide)
|
||||
- Create breathing room between elements
|
||||
- Use scale to create hierarchy
|
||||
|
||||
### Don't
|
||||
|
||||
- Fill empty space with decoration
|
||||
- Use multiple accent colors
|
||||
- Add icons or illustrations unless essential
|
||||
- Create dense information layouts
|
||||
- Add slide numbers, footers, or logos
|
||||
|
||||
## Best For
|
||||
|
||||
Keynotes, philosophical content, product launches
|
||||
Executive briefings, keynote presentations, premium brand communications, minimalist products, investor meetings, high-level strategy
|
||||
|
||||
@@ -1,38 +1,69 @@
|
||||
# notion
|
||||
|
||||
Minimalist hand-drawn line art, intellectual
|
||||
SaaS dashboard aesthetic with clean data focus and productivity tool styling
|
||||
|
||||
## Design Aesthetic
|
||||
|
||||
Simple line doodles with hand-drawn wobble, geometric shapes, maximum whitespace, SaaS product feel
|
||||
Clean, functional SaaS interface aesthetic. Dashboard-inspired layouts with clear data hierarchy. Notion, Linear, and modern productivity tool styling. Information-dense but organized. Professional and trustworthy.
|
||||
|
||||
## Color Palette
|
||||
## Background
|
||||
|
||||
- Background: Pure white (#FFFFFF), off-white (#FAFAFA)
|
||||
- Primary Text: Black (#1A1A1A), dark gray (#4A4A4A)
|
||||
- Accent: Pastel blue (#A8D4F0), Pastel yellow (#F9E79F), Pastel pink (#FADBD8)
|
||||
- Color: Light Gray (#F7F7F5) or Pure White (#FFFFFF)
|
||||
- Texture: None - clean solid backgrounds
|
||||
|
||||
## Typography
|
||||
|
||||
- Primary Font: Clean hand-drawn lettering, simple sans-serif
|
||||
- Secondary Font: Simple sans-serif labels
|
||||
### Primary Font (Headlines)
|
||||
|
||||
System UI stack or Inter. Semi-bold weight for emphasis. Clean, functional letterforms. Slightly tighter letter-spacing.
|
||||
|
||||
### Secondary Font (Body)
|
||||
|
||||
Same family in regular weight. Optimized for screen reading. Comfortable line height (1.5-1.6).
|
||||
|
||||
## Color Palette
|
||||
|
||||
| Role | Color | Hex | Usage |
|
||||
|------|-------|-----|-------|
|
||||
| Background | Light Gray | #F7F7F5 | Primary background |
|
||||
| Card Background | Pure White | #FFFFFF | Content cards |
|
||||
| Primary Text | Near Black | #1F1F1F | Headlines, body |
|
||||
| Secondary Text | Gray | #6B6B6B | Metadata, labels |
|
||||
| Border | Light Border | #E5E5E5 | Card borders, dividers |
|
||||
| Accent Blue | Notion Blue | #2383E2 | Links, primary actions |
|
||||
| Accent Green | Success | #0F7B6C | Positive metrics |
|
||||
| Accent Red | Alert | #E03E3E | Negative metrics |
|
||||
| Accent Yellow | Warning | #DFAB01 | Cautions |
|
||||
|
||||
## Visual Elements
|
||||
|
||||
- Simple line doodles with hand-drawn wobble effect
|
||||
- Geometric shapes
|
||||
- Stick figures
|
||||
- Maximum whitespace
|
||||
- Single-weight ink lines
|
||||
- Card-based layouts with subtle borders or shadows
|
||||
- Clean data tables and charts
|
||||
- Progress bars and metric displays
|
||||
- Icon-based navigation hints
|
||||
- Checkbox and toggle styling
|
||||
- Tag and label chips
|
||||
- Subtle hover state styling
|
||||
- Breadcrumb and hierarchy indicators
|
||||
|
||||
## Style Rules
|
||||
|
||||
- Single color lines (black/dark gray)
|
||||
- 1-2 pastel accents only
|
||||
- NO complex gradients
|
||||
- NO heavy shadows
|
||||
- Imperfect hand-drawn feel
|
||||
### Do
|
||||
|
||||
- Use card-based content organization
|
||||
- Create clear data hierarchy
|
||||
- Use subtle shadows and borders
|
||||
- Keep layouts grid-aligned
|
||||
- Present metrics prominently
|
||||
|
||||
### Don't
|
||||
|
||||
- Use decorative illustrations
|
||||
- Add gradients or complex backgrounds
|
||||
- Create artistic layouts
|
||||
- Use rounded blob shapes
|
||||
- Add slide numbers, footers, or logos
|
||||
|
||||
## Best For
|
||||
|
||||
Knowledge sharing, concept explanations, productivity content, SaaS presentations
|
||||
Product demos, SaaS presentations, productivity tool pitches, metrics dashboards, feature walkthroughs, B2B presentations, technical product marketing
|
||||
|
||||
@@ -1,30 +1,69 @@
|
||||
# playful
|
||||
|
||||
Bold, energetic, engaging
|
||||
Bold, energetic style with vibrant colors and dynamic shapes
|
||||
|
||||
## Design Aesthetic
|
||||
|
||||
Dynamic shapes, vibrant colors, creative layouts
|
||||
Dynamic shapes, vibrant colors, and creative layouts that capture attention and spark joy. Approachable and fun without sacrificing clarity. Perfect for content that needs to educate while entertaining, making complex topics feel accessible and exciting.
|
||||
|
||||
## Color Palette
|
||||
## Background
|
||||
|
||||
- Background: Warm white (#FFFDF7)
|
||||
- Primary Text: Deep purple (#4A1D96)
|
||||
- Accent: Vibrant coral (#FF6B6B)
|
||||
- Color: Warm White (#FFFDF7), soft and inviting
|
||||
- Texture: Light subtle pattern or clean gradient
|
||||
|
||||
## Typography
|
||||
|
||||
- Primary Font: Poppins (headlines)
|
||||
- Secondary Font: Nunito (body)
|
||||
### Primary Font (Headlines)
|
||||
|
||||
Bold rounded sans-serif (Poppins, Nunito Bold, or similar). Friendly, modern, and highly readable. Conveys energy and approachability with geometric character.
|
||||
|
||||
### Secondary Font (Body)
|
||||
|
||||
Rounded sans-serif (Nunito style) for body text. Soft edges create warmth and readability, complementing the playful aesthetic.
|
||||
|
||||
## Color Palette
|
||||
|
||||
| Role | Color | Hex | Usage |
|
||||
|------|-------|-----|-------|
|
||||
| Background | Warm White | #FFFDF7 | Main slide background |
|
||||
| Primary Text | Deep Purple | #4A1D96 | Headlines, key text |
|
||||
| Secondary Text | Dark Purple | #6B3FA0 | Body text |
|
||||
| Primary Accent | Vibrant Coral | #FF6B6B | Primary highlights |
|
||||
| Secondary Accent | Electric Teal | #4ECDC4 | Secondary emphasis |
|
||||
| Tertiary | Sunshine Yellow | #FFE66D | Tertiary accent |
|
||||
| Success | Mint Green | #95E1C3 | Positive elements |
|
||||
| Energy | Hot Pink | #FF69B4 | High energy callouts |
|
||||
|
||||
## Visual Elements
|
||||
|
||||
- Rounded shapes
|
||||
- Gradients
|
||||
- Illustrations
|
||||
- Dynamic compositions
|
||||
- Bright pops
|
||||
- Rounded shapes and soft corners everywhere
|
||||
- Playful gradients (subtle, not overwhelming)
|
||||
- Character illustrations and mascots
|
||||
- Dynamic asymmetrical compositions
|
||||
- Bright color pops and accents
|
||||
- Confetti and celebration elements
|
||||
- Emoji-style icons
|
||||
- Bubble and cloud shapes
|
||||
|
||||
## Style Rules
|
||||
|
||||
### Do
|
||||
|
||||
- Use bold, contrasting colors
|
||||
- Include rounded, friendly shapes
|
||||
- Create dynamic, engaging layouts
|
||||
- Add implied motion and energy
|
||||
- Keep typography large and readable
|
||||
|
||||
### Don't
|
||||
|
||||
- Use sharp, aggressive shapes
|
||||
- Apply dark or somber colors
|
||||
- Create static, boring layouts
|
||||
- Overload with too many elements
|
||||
- Make it look unprofessional
|
||||
- Add slide numbers, footers, or logos
|
||||
|
||||
## Best For
|
||||
|
||||
Creative pitches, educational content, workshops
|
||||
Creative pitches, educational workshops, training materials, community presentations, product launches, children's content, team building, startup culture
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
# retro-flat
|
||||
|
||||
Flat vector illustration with retro palette
|
||||
|
||||
## Design Aesthetic
|
||||
|
||||
Flat vector with clear black outlines (monoline), geometric simplification, toy-model cuteness, NO gradients
|
||||
|
||||
## Color Palette
|
||||
|
||||
- Background: Off-white (#F5F0E6), soft cream
|
||||
- Primary Text: Black (#000000) (clean outlines)
|
||||
- Accent: Retro muted palette
|
||||
- Dusty pink (#E8B4B8)
|
||||
- Sage green (#A8C686)
|
||||
- Mustard yellow (#E8B84A)
|
||||
- Muted blue (#7BA7BC)
|
||||
|
||||
## Typography
|
||||
|
||||
- Primary Font: Bold geometric sans-serif with black outline
|
||||
- Secondary Font: Clean sans-serif
|
||||
|
||||
## Visual Elements
|
||||
|
||||
- Clear black monoline outlines (uniform width)
|
||||
- Flat color fills with NO gradients
|
||||
- Geometric simplified shapes
|
||||
- Toy-like proportions
|
||||
- Minimal shadows
|
||||
- Vintage badge style
|
||||
- Halftone dots (optional)
|
||||
|
||||
## Style Rules
|
||||
|
||||
- MUST use clear black outlines on all elements
|
||||
- NO 3D effects
|
||||
- NO gradients
|
||||
- Simple flat color blocks only
|
||||
|
||||
## Best For
|
||||
|
||||
Tutorials, explainers, product introductions, educational content
|
||||
@@ -0,0 +1,66 @@
|
||||
# sketch-notes
|
||||
|
||||
Soft hand-drawn illustration style with fresh, refined minimalist editorial aesthetic
|
||||
|
||||
## Design Aesthetic
|
||||
|
||||
Illustration or hand-drawn feel with soft, relaxed brush strokes. Fresh, refined overall style with minimalist editorial approach. Emphasis on precision, clarity and intelligent elegance while prioritizing warmth, approachability and friendliness.
|
||||
|
||||
## Background
|
||||
|
||||
- Color: Warm Off-White (#FAF8F0)
|
||||
- Texture: Subtle paper grain, slightly warm tone to avoid clinical feel
|
||||
|
||||
## Typography
|
||||
|
||||
### Primary Font (Headlines)
|
||||
|
||||
Bold hand-written marker font or cartoon poster font. Slightly uneven baseline for organic feel. Thick strokes with soft edges. Render as hand-drawn letters, not typed text.
|
||||
|
||||
### Secondary Font (Body)
|
||||
|
||||
Clear handwritten round or hard-pen style mimicking everyday notes. Consistent sizing with slight natural variation. Render as casual handwriting, legible but not mechanical.
|
||||
|
||||
## Color Palette
|
||||
|
||||
| Role | Color | Hex | Usage |
|
||||
|------|-------|-----|-------|
|
||||
| Background | Warm Off-White | #FAF8F0 | Primary background |
|
||||
| Primary Text | Deep Charcoal | #2C3E50 | Headlines, body text |
|
||||
| Alt Text | Deep Brown | #4A4A4A | Secondary text elements |
|
||||
| Accent 1 | Soft Orange | #F4A261 | Highlights, emphasis |
|
||||
| Accent 2 | Mustard Yellow | #E9C46A | Secondary highlights |
|
||||
| Accent 3 | Sage Green | #87A96B | Nature, growth concepts |
|
||||
| Accent 4 | Light Blue | #7EC8E3 | Tech, AI elements |
|
||||
| Accent 5 | Red Brown | #A0522D | Land, infrastructure |
|
||||
|
||||
## Visual Elements
|
||||
|
||||
- Connection lines with hand-drawn wavy feel, not perfectly straight
|
||||
- Conceptual abstract icons illustrating ideas rather than literal scenes
|
||||
- Color fills don't need to completely fill outlines - preserve hand-painted casual feel
|
||||
- Simple geometric shapes with rounded corners
|
||||
- Arrows and pointers with sketchy, informal style
|
||||
- Doodle-style decorative elements: stars, spirals, underlines
|
||||
|
||||
## Style Rules
|
||||
|
||||
### Do
|
||||
|
||||
- Keep layouts open and well-structured
|
||||
- Emphasize information hierarchy and readability
|
||||
- Use hand-drawn quality for all elements
|
||||
- Allow imperfection - slight wobbles add character
|
||||
- Layer elements with subtle overlaps
|
||||
|
||||
### Don't
|
||||
|
||||
- Use perfect geometric shapes
|
||||
- Create photorealistic elements
|
||||
- Overcrowd with too many elements
|
||||
- Use pure white backgrounds
|
||||
- Add slide numbers, footers, or logos
|
||||
|
||||
## Best For
|
||||
|
||||
Educational content, knowledge sharing, technical explanations, friendly presentations, tutorials, onboarding materials
|
||||
@@ -1,29 +1,67 @@
|
||||
# storytelling
|
||||
|
||||
Narrative-driven, cinematic, immersive
|
||||
Cinematic narrative style with full-bleed visuals and emotional impact
|
||||
|
||||
## Design Aesthetic
|
||||
|
||||
Full-bleed imagery, dramatic typography, chapter-like flow
|
||||
Cinematic, full-bleed imagery that creates emotional connection. Story-driven compositions that guide the viewer through a narrative arc. Each slide is a scene in a larger story. Photography and illustration work together to create atmosphere.
|
||||
|
||||
## Color Palette
|
||||
## Background
|
||||
|
||||
- Background: Rich charcoal (#2D2D2D) or full-bleed imagery
|
||||
- Primary Text: Warm white (#FAF9F6)
|
||||
- Accent: Warm gold (#D4AF37)
|
||||
- Color: Varies by mood - dark for drama (#0F0F0F), warm for comfort (#FDF8F3)
|
||||
- Texture: Photographic or illustrated backgrounds, subtle vignetting
|
||||
|
||||
## Typography
|
||||
|
||||
- Primary Font: Playfair Display (headlines)
|
||||
- Secondary Font: Lora (body)
|
||||
### Primary Font (Headlines)
|
||||
|
||||
Elegant serif like Playfair Display, Libre Baskerville, or editorial serif. Large, expressive headlines that complement imagery. Can use italic for emotional emphasis.
|
||||
|
||||
### Secondary Font (Body)
|
||||
|
||||
Clean sans-serif for contrast and readability. Light weight to not compete with imagery. White or dark depending on background.
|
||||
|
||||
## Color Palette
|
||||
|
||||
| Role | Color | Hex | Usage |
|
||||
|------|-------|-----|-------|
|
||||
| Background Dark | Near Black | #0F0F0F | Dramatic scenes |
|
||||
| Background Warm | Warm Cream | #FDF8F3 | Comfortable scenes |
|
||||
| Primary Text Light | Off-White | #FAFAFA | Text on dark |
|
||||
| Primary Text Dark | Deep Charcoal | #1F1F1F | Text on light |
|
||||
| Accent Warm | Golden | #D4A853 | Warm highlights |
|
||||
| Accent Cool | Teal | #2DD4BF | Cool accents |
|
||||
| Overlay | Black 40% | rgba(0,0,0,0.4) | Text legibility |
|
||||
|
||||
## Visual Elements
|
||||
|
||||
- Full-bleed photos
|
||||
- Dramatic typography
|
||||
- Chapter markers
|
||||
- Emotional imagery
|
||||
- Full-bleed photographic or illustrated backgrounds
|
||||
- Cinematic aspect compositions (rule of thirds)
|
||||
- Text overlays with subtle background darkening
|
||||
- Atmospheric lighting and mood
|
||||
- People and human elements when possible
|
||||
- Emotional color grading
|
||||
- Subtle motion blur or depth of field effects
|
||||
- Vignetting to draw focus
|
||||
|
||||
## Style Rules
|
||||
|
||||
### Do
|
||||
|
||||
- Create emotional connection through imagery
|
||||
- Use photography or illustration as primary element
|
||||
- Let visuals tell the story
|
||||
- Create consistent mood throughout deck
|
||||
- Use text sparingly - images speak
|
||||
|
||||
### Don't
|
||||
|
||||
- Use stock photo aesthetic
|
||||
- Crowd slides with text
|
||||
- Mix conflicting visual moods
|
||||
- Use clip art or basic shapes
|
||||
- Add slide numbers, footers, or logos
|
||||
|
||||
## Best For
|
||||
|
||||
Case studies, journey narratives, brand stories
|
||||
Case studies, brand stories, narrative presentations, emotional pitches, documentary-style content, origin stories, customer journeys
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
# technical
|
||||
|
||||
Precise, data-driven, analytical
|
||||
|
||||
## Design Aesthetic
|
||||
|
||||
Blueprint style, schematic diagrams, grid layouts
|
||||
|
||||
## Color Palette
|
||||
|
||||
- Background: Dark charcoal (#1A1A2E)
|
||||
- Primary Text: Light gray (#E8E8E8)
|
||||
- Accent: Cyan (#00D4FF)
|
||||
|
||||
## Typography
|
||||
|
||||
- Primary Font: JetBrains Mono (headlines)
|
||||
- Secondary Font: IBM Plex Sans (body)
|
||||
|
||||
## Visual Elements
|
||||
|
||||
- Grid overlays
|
||||
- Circuit patterns
|
||||
- Data visualizations
|
||||
- Monospace code blocks
|
||||
|
||||
## Best For
|
||||
|
||||
Technical documentation, engineering presentations
|
||||
@@ -0,0 +1,72 @@
|
||||
# vector-illustration
|
||||
|
||||
Flat vector illustration style with clear black outlines and retro soft color palette
|
||||
|
||||
## Design Aesthetic
|
||||
|
||||
Flat vector illustration with no gradients or 3D effects. Clear, uniform-thickness black outlines on all elements. Geometric simplification reducing complex objects to basic shapes. Toy model aesthetic that's cute, playful, and approachable. Panoramic horizontal compositions work well.
|
||||
|
||||
## Background
|
||||
|
||||
- Color: Cream Off-White (#F5F0E6)
|
||||
- Texture: Subtle paper texture, warm nostalgic feel reminiscent of vintage prints
|
||||
|
||||
## Typography
|
||||
|
||||
### Primary Font (Headlines)
|
||||
|
||||
Large, bold retro serif for titles conveying authority and elegance. Think classic advertising posters. Clean letterforms with strong presence.
|
||||
|
||||
### Secondary Font (Subtitles)
|
||||
|
||||
All-caps sans-serif inside colored rectangular blocks. Label-like appearance. High contrast against block color.
|
||||
|
||||
### Body Font
|
||||
|
||||
Clean geometric sans-serif for readability. Futura, Avenir, or similar. Consistent weight throughout.
|
||||
|
||||
## Color Palette
|
||||
|
||||
| Role | Color | Hex | Usage |
|
||||
|------|-------|-----|-------|
|
||||
| Background | Cream Off-White | #F5F0E6 | Primary background |
|
||||
| Outlines | Deep Charcoal | #2D2D2D | All element outlines |
|
||||
| Primary Text | Black | #1A1A1A | Headlines, body |
|
||||
| Accent 1 | Coral Red | #E07A5F | Primary accent, warmth |
|
||||
| Accent 2 | Mint Green | #81B29A | Secondary accent, nature |
|
||||
| Accent 3 | Mustard Yellow | #F2CC8F | Highlights, energy |
|
||||
| Accent 4 | Burnt Orange | #D4764A | Tertiary accent |
|
||||
| Accent 5 | Rock Blue | #577590 | Cool balance, tech |
|
||||
|
||||
## Visual Elements
|
||||
|
||||
- All objects have closed black outlines (coloring book style)
|
||||
- Rounded line endings, avoid sharp corners
|
||||
- Trees simplified to lollipop or triangle shapes
|
||||
- Buildings simplified to rectangular blocks with grid windows
|
||||
- 2.5D perspective (isometric-like but more free-form)
|
||||
- Depth through layering and overlap, not atmospheric perspective
|
||||
- Decorative geometric elements: radiating lines (sunbursts), pill-shaped clouds, dots, stars
|
||||
- People as simple geometric figures with minimal facial detail
|
||||
|
||||
## Style Rules
|
||||
|
||||
### Do
|
||||
|
||||
- Maintain consistent outline thickness throughout
|
||||
- Use soft, vintage color palette
|
||||
- Simplify all objects to basic geometric shapes
|
||||
- Create depth through layering
|
||||
- Add playful decorative elements
|
||||
|
||||
### Don't
|
||||
|
||||
- Use gradients or realistic shading
|
||||
- Create photorealistic elements
|
||||
- Use thin or varying line weights
|
||||
- Include complex detailed illustrations
|
||||
- Add slide numbers, footers, or logos
|
||||
|
||||
## Best For
|
||||
|
||||
Educational presentations, creative proposals, children's content, brand showcases, warm approachable topics, explainer content
|
||||
@@ -1,32 +1,68 @@
|
||||
# warm
|
||||
|
||||
Cozy, healing, hand-drawn illustration style
|
||||
Soft gradients and wellness aesthetic with approachable, friendly feeling
|
||||
|
||||
## Design Aesthetic
|
||||
|
||||
Soft hand-drawn illustrations, cozy and healing atmosphere, gentle curves, watercolor textures
|
||||
Soft, calming gradients with rounded shapes. Wellness and lifestyle aesthetic that feels approachable and nurturing. Organic flowing forms balanced with clean typography. Warmth and comfort in every element.
|
||||
|
||||
## Color Palette
|
||||
## Background
|
||||
|
||||
- Background: Cream gradient (#FFF8E7 → #FFE4C4)
|
||||
- Primary Text: Warm brown (#5D4037)
|
||||
- Accent: Soft coral (#FF8A80), Peachy pink (#FFAB91)
|
||||
- Color: Soft gradient from Warm Peach (#FDF4F0) to Soft Lavender (#F5F0FF)
|
||||
- Texture: Subtle organic shapes, soft blurred elements
|
||||
|
||||
## Typography
|
||||
|
||||
- Primary Font: Rounded hand-drawn style lettering
|
||||
- Secondary Font: Soft serif hand lettering
|
||||
### Primary Font (Headlines)
|
||||
|
||||
Rounded sans-serif like Nunito, Poppins, or Quicksand. Medium to semi-bold weight. Friendly, approachable letterforms. Generous spacing.
|
||||
|
||||
### Secondary Font (Body)
|
||||
|
||||
Same family in regular weight, or complementary rounded sans-serif. Comfortable reading size. Warm, inviting tone.
|
||||
|
||||
## Color Palette
|
||||
|
||||
| Role | Color | Hex | Usage |
|
||||
|------|-------|-----|-------|
|
||||
| Background Start | Warm Peach | #FDF4F0 | Gradient start |
|
||||
| Background End | Soft Lavender | #F5F0FF | Gradient end |
|
||||
| Primary Text | Warm Charcoal | #3D3D3D | Headlines, body |
|
||||
| Secondary Text | Soft Gray | #6B6B6B | Supporting text |
|
||||
| Accent 1 | Coral | #FF8A80 | Primary highlights |
|
||||
| Accent 2 | Soft Teal | #80CBC4 | Balance, growth |
|
||||
| Accent 3 | Lavender | #B39DDB | Calm, premium |
|
||||
| Accent 4 | Soft Yellow | #FFE082 | Energy, joy |
|
||||
|
||||
## Visual Elements
|
||||
|
||||
- Soft watercolor textures
|
||||
- Gentle curves
|
||||
- Cozy illustrations
|
||||
- Warm lighting
|
||||
- Hearts and stars
|
||||
- Plant motifs
|
||||
- Cute characters
|
||||
- Soft gradient backgrounds (avoid harsh transitions)
|
||||
- Organic blob shapes with blurred edges
|
||||
- Rounded rectangles and pill shapes
|
||||
- Soft drop shadows (large blur, low opacity)
|
||||
- Simple line illustrations with rounded endpoints
|
||||
- Abstract organic patterns
|
||||
- Floating elements with gentle motion implied
|
||||
- Layered translucent shapes
|
||||
|
||||
## Style Rules
|
||||
|
||||
### Do
|
||||
|
||||
- Use soft, gentle color transitions
|
||||
- Keep all corners rounded
|
||||
- Create sense of calm and space
|
||||
- Use organic, flowing shapes
|
||||
- Layer elements with transparency
|
||||
|
||||
### Don't
|
||||
|
||||
- Use sharp corners or harsh lines
|
||||
- Create high-contrast jarring combinations
|
||||
- Use dark or aggressive colors
|
||||
- Add busy or complex illustrations
|
||||
- Add slide numbers, footers, or logos
|
||||
|
||||
## Best For
|
||||
|
||||
Personal growth, wellness, emotional content, lifestyle topics
|
||||
Wellness content, personal development, lifestyle brands, health and fitness, mindfulness, self-care, coaching, therapy, HR presentations
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
import { existsSync, readdirSync, readFileSync } from "fs";
|
||||
import { join, basename, extname } from "path";
|
||||
import PptxGenJS from "pptxgenjs";
|
||||
|
||||
interface SlideInfo {
|
||||
filename: string;
|
||||
path: string;
|
||||
index: number;
|
||||
promptPath?: string;
|
||||
}
|
||||
|
||||
function parseArgs(): { dir: string; output?: string } {
|
||||
const args = process.argv.slice(2);
|
||||
let dir = "";
|
||||
let output: string | undefined;
|
||||
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
if (args[i] === "--output" || args[i] === "-o") {
|
||||
output = args[++i];
|
||||
} else if (!args[i].startsWith("-")) {
|
||||
dir = args[i];
|
||||
}
|
||||
}
|
||||
|
||||
if (!dir) {
|
||||
console.error("Usage: bun merge-to-pptx.ts <slide-deck-dir> [--output filename.pptx]");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
return { dir, output };
|
||||
}
|
||||
|
||||
function findSlideImages(dir: string): SlideInfo[] {
|
||||
if (!existsSync(dir)) {
|
||||
console.error(`Directory not found: ${dir}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const files = readdirSync(dir);
|
||||
const slidePattern = /^(\d+)-slide-.*\.(png|jpg|jpeg)$/i;
|
||||
const promptsDir = join(dir, "prompts");
|
||||
const hasPrompts = existsSync(promptsDir);
|
||||
|
||||
const slides: SlideInfo[] = files
|
||||
.filter((f) => slidePattern.test(f))
|
||||
.map((f) => {
|
||||
const match = f.match(slidePattern);
|
||||
const baseName = f.replace(/\.(png|jpg|jpeg)$/i, "");
|
||||
const promptPath = hasPrompts ? join(promptsDir, `${baseName}.md`) : undefined;
|
||||
|
||||
return {
|
||||
filename: f,
|
||||
path: join(dir, f),
|
||||
index: parseInt(match![1], 10),
|
||||
promptPath: promptPath && existsSync(promptPath) ? promptPath : undefined,
|
||||
};
|
||||
})
|
||||
.sort((a, b) => a.index - b.index);
|
||||
|
||||
if (slides.length === 0) {
|
||||
console.error(`No slide images found in: ${dir}`);
|
||||
console.error("Expected format: 01-slide-*.png, 02-slide-*.png, etc.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
return slides;
|
||||
}
|
||||
|
||||
function findBasePrompt(): string | undefined {
|
||||
const scriptDir = import.meta.dir;
|
||||
const basePromptPath = join(scriptDir, "..", "references", "base-prompt.md");
|
||||
if (existsSync(basePromptPath)) {
|
||||
return readFileSync(basePromptPath, "utf-8");
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async function createPptx(slides: SlideInfo[], outputPath: string) {
|
||||
const pptx = new PptxGenJS();
|
||||
|
||||
pptx.layout = "LAYOUT_16x9";
|
||||
pptx.author = "baoyu-slide-deck";
|
||||
pptx.subject = "Generated Slide Deck";
|
||||
|
||||
const basePrompt = findBasePrompt();
|
||||
let notesCount = 0;
|
||||
|
||||
for (const slide of slides) {
|
||||
const s = pptx.addSlide();
|
||||
const imageData = readFileSync(slide.path);
|
||||
const base64 = imageData.toString("base64");
|
||||
const ext = extname(slide.filename).toLowerCase().replace(".", "");
|
||||
const mimeType = ext === "png" ? "image/png" : "image/jpeg";
|
||||
|
||||
s.addImage({
|
||||
data: `data:${mimeType};base64,${base64}`,
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: "100%",
|
||||
h: "100%",
|
||||
sizing: { type: "cover", w: "100%", h: "100%" },
|
||||
});
|
||||
|
||||
if (slide.promptPath) {
|
||||
const slidePrompt = readFileSync(slide.promptPath, "utf-8");
|
||||
const fullNotes = basePrompt ? `${basePrompt}\n\n---\n\n${slidePrompt}` : slidePrompt;
|
||||
s.addNotes(fullNotes);
|
||||
notesCount++;
|
||||
}
|
||||
|
||||
console.log(`Added: ${slide.filename}${slide.promptPath ? " (with notes)" : ""}`);
|
||||
}
|
||||
|
||||
await pptx.writeFile({ fileName: outputPath });
|
||||
console.log(`\nCreated: ${outputPath}`);
|
||||
console.log(`Total slides: ${slides.length}`);
|
||||
if (notesCount > 0) {
|
||||
console.log(`Slides with notes: ${notesCount}${basePrompt ? " (includes base prompt)" : ""}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const { dir, output } = parseArgs();
|
||||
const slides = findSlideImages(dir);
|
||||
|
||||
const dirName = basename(dir) === "slide-deck" ? basename(join(dir, "..")) : basename(dir);
|
||||
const outputPath = output || join(dir, `${dirName}.pptx`);
|
||||
|
||||
console.log(`Found ${slides.length} slides in: ${dir}\n`);
|
||||
|
||||
await createPptx(slides, outputPath);
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error("Error:", err.message);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -261,8 +261,14 @@ Style notes: [style-specific characteristics]
|
||||
1. Check available image generation skills
|
||||
2. If multiple skills available, ask user to choose
|
||||
|
||||
**Session Management**:
|
||||
If the image generation skill supports `--sessionId`:
|
||||
1. Generate a unique session ID at the start (e.g., `xhs-{topic-slug}-{timestamp}`)
|
||||
2. Use the same session ID for all images in the series
|
||||
3. This ensures style consistency across all generated images
|
||||
|
||||
**Generation Flow**:
|
||||
1. Call selected image generation skill with prompt file and output path
|
||||
1. Call selected image generation skill with prompt file, output path, and session ID
|
||||
2. Confirm generation success
|
||||
3. Report progress: "Generated X/N"
|
||||
4. Continue to next
|
||||
|
||||
Reference in New Issue
Block a user