Initial commit: Xiaohongshu Images Skill

- Add SKILL.md with complete skill definition
- Add default HTML/CSS prompt template for styled article pages
- Add Gemini API image generation script for cover images
- Add Playwright screenshot script with 3:4 ratio capture
- Add .env.example and .gitignore configuration

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
James Gong
2026-01-14 16:22:37 +11:00
commit 955439d133
7 changed files with 1325 additions and 0 deletions
+17
View File
@@ -0,0 +1,17 @@
# Xiaohongshu Images Skill - Environment Configuration
#
# Instructions:
# 1. Copy this file to .env
# 2. Fill in your API key
# 3. The .env file is gitignored and will not be committed to version control
#
# ============================================================================
# ============================================================================
# Image Generation Configuration
# ============================================================================
# Google Gemini API Key
# Obtain from: https://aistudio.google.com/app/apikey
# Used for generating cover images with AI
GEMINI_API_KEY=your_gemini_api_key_here
+65
View File
@@ -0,0 +1,65 @@
# Environment variables (contains API keys, should not be committed)
.env
.env.local
.env.*.local
# Output directory (generated content)
output/
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
# Virtual environments
venv/
ENV/
env/
.venv/
# IDE and editors
.idea/
.vscode/
*.swp
*.swo
*.sublime-workspace
*.sublime-project
# macOS
.DS_Store
.AppleDouble
.LSOverride
._*
# Windows
Thumbs.db
ehthumbs.db
Desktop.ini
# Playwright
.playwright-mcp/
# Temporary files
*.tmp
*.temp
*.log
# Package artifacts
*.skill
+208
View File
@@ -0,0 +1,208 @@
# Xiaohongshu Images Skill
A Claude Code skill that transforms markdown, HTML, or text content into beautifully styled HTML pages with AI-generated cover images, then captures them as sequential screenshots at 3:4 ratio for Xiaohongshu posting.
## Features
- **Content Processing**: Accepts markdown, HTML, or plain text content
- **AI Cover Images**: Generates editorial-style cover illustrations using Google Gemini
- **Styled HTML Output**: Creates beautifully formatted HTML pages with modern typography
- **Screenshot Capture**: Takes sequential 3:4 ratio screenshots optimized for Xiaohongshu
- **Smart Text Boundaries**: Ensures no text is cut off in screenshots
## Installation
### Prerequisites
- Python 3.8 or higher
- Claude Code CLI
### Setup
1. **Clone or copy this skill to your Claude skills directory:**
```bash
# Copy to global skills
cp -r xiaohongshu-images-skill ~/.claude/skills/
# Or symlink for development
ln -s /path/to/xiaohongshu-images-skill ~/.claude/skills/xiaohongshu-images-skill
```
2. **Install Python dependencies:**
```bash
pip install python-dotenv playwright
playwright install chromium
```
3. **Configure environment variables:**
```bash
cd ~/.claude/skills/xiaohongshu-images-skill
cp .env.example .env
# Edit .env and add your GEMINI_API_KEY
```
4. **Get your Gemini API Key:**
Visit [Google AI Studio](https://aistudio.google.com/app/apikey) to obtain your API key.
## Usage
### Via Claude Code
Invoke the skill in Claude Code:
```
/xiaohongshu-images
```
Then provide your content:
- Paste markdown/HTML content directly
- Provide a file path: `/path/to/article.md`
- Provide a URL to fetch content from
### Example
```markdown
/xiaohongshu-images
# My Article Title
This is the introduction paragraph explaining the topic...
## Section 1
Content for section 1 with detailed explanation...
## Section 2
More content here with examples...
```
### Output
The skill generates:
- `output/<date-title>/index.html` - Styled HTML page
- `output/<date-title>/images/cover.png` - AI-generated cover image
- `output/<date-title>/screenshots/01.png, 02.png, ...` - Sequential screenshots
## Directory Structure
```
xiaohongshu-images-skill/
├── SKILL.md # Main skill definition
├── README.md # This file
├── prompts/
│ └── default.md # Default HTML/CSS styling prompt
├── scripts/
│ ├── generate_images.py # Gemini image generation
│ └── screenshot.py # Screenshot capture
├── output/ # Generated outputs (gitignored)
├── .env # Environment variables (gitignored)
├── .env.example # Environment template
└── .gitignore
```
## Customization
### Custom Prompt Templates
Create custom styling templates in the `prompts/` directory:
1. Create a new `.md` file (e.g., `prompts/minimal.md`)
2. Define your HTML/CSS specifications
3. Invoke with: "Use the minimal template for this article"
### Modifying Styles
Edit `prompts/default.md` to customize:
- Card dimensions and colors
- Font families and sizes
- Typography hierarchy
- Code block styling
- Responsive breakpoints
## Configuration
### Environment Variables
| Variable | Description | Required |
|----------|-------------|----------|
| `GEMINI_API_KEY` | Google Gemini API key for image generation | Yes |
### Screenshot Settings
Default screenshot dimensions (3:4 ratio for Xiaohongshu):
- Width: 1080px
- Height: 1440px
- Scale factor: 2x (Retina quality)
To modify, edit `scripts/screenshot.py`:
```python
SCREENSHOT_WIDTH = 1080
SCREENSHOT_HEIGHT = 1440
```
## Scripts
### generate_images.py
Generates cover images using Google Gemini API.
```bash
python scripts/generate_images.py output/<folder>/prompts.json
```
JSON format:
```json
{
"theme": "Article theme for cover image generation"
}
```
### screenshot.py
Captures sequential screenshots of HTML pages.
```bash
python scripts/screenshot.py output/<folder>/index.html
```
Features:
- Automatic page scrolling
- Smart text boundary detection
- No text cut-off at boundaries
- 3:4 aspect ratio output
## Troubleshooting
### Gemini API Issues
- Verify your API key is correctly set in `.env`
- Check API quotas at [Google AI Studio](https://aistudio.google.com/)
- Ensure the API key has access to image generation models
### Screenshot Issues
- Install Playwright browsers: `playwright install chromium`
- Check file paths are correct
- Ensure HTML file is valid and accessible
### Font Loading
If fonts don't load in screenshots:
- Increase wait time in `screenshot.py`
- Check Google Fonts availability
- Consider using local fonts
## License
MIT License - See LICENSE file for details.
## Related Skills
- `chinese-viral-writer` - Chinese viral content creation
- `wechat-article-formatter` - WeChat article formatting
- `wechat-article-publisher` - WeChat publishing automation
+267
View File
@@ -0,0 +1,267 @@
---
name: xiaohongshu-images
description: Generate beautifully styled HTML pages from markdown/HTML/txt content with cover images using Gemini AI, then capture screenshots at 3:4 ratio for Xiaohongshu. Use when user wants to create styled article pages, generate article images, or prepare content for Xiaohongshu platform.
---
# Xiaohongshu Images Skill
This skill transforms markdown, HTML, or text content into beautifully styled HTML pages with AI-generated cover images, then captures them as sequential screenshots at 3:4 ratio for Xiaohongshu posting.
## Overview
The skill performs the following workflow:
1. **Accept Content**: Receives markdown, HTML, or txt format content from the user
2. **Load Prompt Template**: Reads the prompt template from `prompts/default.md` in this skill's directory
3. **Generate Cover Image**: Uses Gemini API to generate a cover image based on the article content
4. **Generate HTML**: Creates a beautifully styled HTML page following the prompt template specifications
5. **Save Output**: Saves the HTML to `/output/<date-article-title>/index.html`
6. **Capture Screenshots**: Takes sequential 3:4 ratio screenshots of the entire page without cutting text
## Usage
When the user invokes this skill, follow these steps:
### Step 1: Identify the Input
The user will provide one of the following:
- A file path to a markdown, HTML, or txt file (e.g., `/path/to/article.md`)
- Raw content directly in the conversation
- A URL to fetch content from
If the input is unclear, ask the user to provide either a file path, URL, or paste the content directly.
### Step 2: Read the Prompt Template
Read the prompt template from this skill's directory:
```
{{SKILL_DIR}}/prompts/default.md
```
Use the Read tool to get the prompt template content. This template defines the HTML/CSS styling specifications.
### Step 3: Extract Article Title and Date
From the content, extract:
- **Title**: The main heading (h1) or first significant title in the content
- **Date**: Current date in YYYY-MM-DD format
Create the output folder name as: `<date>-<sanitized-title>`
- Replace spaces with hyphens
- Remove special characters
- Keep it reasonably short (max 50 characters)
### Step 4: Generate Cover Image with Gemini
If the prompt template specifies image generation requirements (which it does by default):
1. **Read the environment variables** from `{{SKILL_DIR}}/.env` to get `GEMINI_API_KEY`
2. **Analyze the article content** to extract the main theme
3. **Generate image prompt** based on the template:
- Style: Hand-drawn illustration similar to *The New Yorker* editorial cartoons
- Content: Visual representation of the article's main theme
- Dimensions: 600px × 350px (will be scaled to fit)
4. **Call Gemini API** using the generate_images.py script:
```bash
cd {{SKILL_DIR}} && python scripts/generate_images.py output/<folder-name>/prompts.json
```
Or use direct API call via curl:
```bash
curl -X POST "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash-exp:generateContent?key=${GEMINI_API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"contents": [{
"parts": [{"text": "<image_prompt>"}]
}],
"generationConfig": {
"responseModalities": ["TEXT", "IMAGE"]
}
}'
```
5. **Save the generated image** to `output/<folder-name>/images/cover.png`
### Step 5: Generate HTML
Using the prompt template and the user's content:
1. **Parse the content** to identify:
- Title (h1)
- Subtitles (h2-h6)
- Paragraphs
- Lists
- Code blocks
- Links
- Emphasis/bold text
- Blockquotes
2. **Generate complete HTML** following the template specifications:
- Dark gradient background
- 600px × 800px cream-colored card
- Proper typography with Google Fonts (Noto Serif SC, Inter, JetBrains Mono)
- Cover image at the top
- All specified styling for text, links, lists, code blocks, etc.
- Responsive design for mobile
3. **Important HTML Structure**:
```html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Article Title</title>
<!-- Google Fonts -->
<link href="https://fonts.googleapis.com/css2?family=Noto+Serif+SC:wght@700&family=Inter:wght@300;400;700;800&family=JetBrains+Mono:wght@400;700&display=swap" rel="stylesheet">
<style>
/* All CSS styles inline */
</style>
</head>
<body>
<div class="container">
<img src="images/cover.png" class="cover-image" alt="Cover">
<div class="content">
<!-- Article content -->
</div>
</div>
</body>
</html>
```
4. **Save the HTML** to `output/<folder-name>/index.html`
### Step 6: Take Screenshots
After generating the HTML, capture sequential screenshots at 3:4 ratio (e.g., 1080×1440 pixels):
1. **Open the HTML page** using Playwright browser
2. **Calculate screenshot sections**:
- Screenshot height: 1440px (at 1080px width for 3:4 ratio)
- Total page height / screenshot height = number of screenshots needed
3. **For each screenshot**:
- Ensure no text is cut off at boundaries
- If text would be cut, move the boundary to before that line and leave whitespace
- Use smart text detection to find safe cutting points
4. **Save screenshots** to `output/<folder-name>/screenshots/`:
- `01.png`, `02.png`, `03.png`, etc.
Use the screenshot script:
```bash
cd {{SKILL_DIR}} && python scripts/screenshot.py output/<folder-name>/index.html
```
### Step 7: Report Results
After completion, report to the user:
- HTML file location
- Number of screenshots generated
- Screenshots folder location
- Preview of the first screenshot (if possible)
## Directory Structure
```
{{SKILL_DIR}}/
├── SKILL.md # This file
├── prompts/
│ └── default.md # Default HTML/CSS styling prompt
├── scripts/
│ ├── generate_images.py # Gemini image generation script
│ └── screenshot.py # Screenshot capture script
├── output/ # Generated outputs (gitignored)
│ └── <date-title>/
│ ├── index.html
│ ├── images/
│ │ └── cover.png
│ └── screenshots/
│ ├── 01.png
│ ├── 02.png
│ └── ...
├── .env # Environment variables (gitignored)
├── .env.example # Environment variable template
└── .gitignore
```
## Environment Variables
Required environment variables in `.env`:
```
GEMINI_API_KEY=your_gemini_api_key_here
```
Get your API key from: https://aistudio.google.com/app/apikey
## Example Workflow
**User:** Create a styled article page from this markdown:
```markdown
# My Article Title
This is the introduction paragraph...
## Section 1
Content for section 1...
```
**Assistant Actions:**
1. Read prompt template from `prompts/default.md`
2. Extract title: "My Article Title"
3. Create output folder: `output/2024-01-14-my-article-title/`
4. Generate cover image using Gemini API based on article theme
5. Generate styled HTML following template specifications
6. Save to `output/2024-01-14-my-article-title/index.html`
7. Open in browser and take 3:4 ratio screenshots
8. Save screenshots to `output/2024-01-14-my-article-title/screenshots/`
9. Report completion with file locations
## Custom Prompt Templates
Users can provide custom prompt templates by:
1. Placing a `.md` file in the `prompts/` directory
2. Specifying the template name when invoking the skill
Example: "Use the `xiaohongshu-style` template for this article"
## Error Handling
If the Gemini API call fails:
1. Display the error message to the user
2. Offer to retry or proceed without cover image
3. If proceeding without image, use a placeholder or omit the cover
If screenshot capture fails:
1. Verify the HTML file exists and is valid
2. Check browser dependencies
3. Report the specific error to the user
## Dependencies
This skill requires:
- Python 3.8+
- `python-dotenv` package
- Playwright for screenshot capture (installed via pip: `pip install playwright && playwright install chromium`)
Install dependencies:
```bash
pip install python-dotenv playwright
playwright install chromium
```
## Notes
- The skill preserves all original content exactly as provided
- No modifications, simplifications, or deletions to the content
- The cover image is generated based on the article's main theme
- Screenshots are optimized for Xiaohongshu's 3:4 aspect ratio
- Text is never cut off in screenshots - boundaries are adjusted intelligently
+241
View File
@@ -0,0 +1,241 @@
# HTML/CSS Article Image Expert Prompt
You are a frontend development and web layout expert proficient in HTML/CSS.
## Task Objective
Please carefully read the article link or article content provided by the user, and generate a complete HTML page according to the following style specifications. The page should be presented as a cream-colored card on a dark background, with a modern feel and good reading experience.
---
## 1. Overall Layout
### 1.1 Page Background
- **Dark gradient background**
```css
background: linear-gradient(135deg, #1e1e2e 0%, #2d2b55 50%, #3e3a5f 100%);
background-attachment: fixed;
```
- **Layout method**: Use Flexbox to achieve vertical and horizontal centering
### 1.2 Main Container (Cream-colored Card)
- **Dimensions**: 600px × 800px
- **Background color**: `#F9F9F6`
- **border-radius**: 0px (card has no rounded corners, rectangular with right angles)
- **3D shadow** (three layers):
```css
box-shadow:
0 25px 50px rgba(0, 0, 0, 0.4),
0 10px 30px rgba(0, 10, 20, 0.3),
0 5px 15px rgba(0, 5, 15, 0.25);
```
### 1.3 Content Area
- **Content area scope**: Cover image, title, and body text. Note: Cover is part of the content area and scrolls with the content; NEVER use `position: fixed` or `position: sticky` to make the cover image hover.
- **Padding**: `20px 50px 50px 50px` (top, right, bottom, left - top padding reduced to 20px)
- **Scrolling**: Vertical scrolling enabled
- **Custom scrollbar**: Fully transparent scrollbar or no scrollbar display
- **CSS Implementation Key Points**:
- `.container` set `overflow-y: auto`
- `.content` only sets `padding`, no scrolling
- Cover image as direct child element of `.container`, positioned before `.content`
---
## 2. Font System
### 2.1 Import Fonts
Import the following fonts from Google Fonts:
- **Noto Serif SC** (Source Han Serif): `weight: 700`
- **Inter**: `weight: 300, 400, 700, 800`
- **JetBrains Mono**: `weight: 400, 700`
### 2.2 Font Application Rules
| Content Type | Font |
|--------------|------|
| Body default | System font stack (`-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto`, etc.) |
| H1 Main title | Noto Serif SC (Source Han Serif) |
| H2 Subtitle | Times New Roman |
| English titles | Inter |
| Code | JetBrains Mono |
---
## 3. Text Style Specifications
### 3.1 Cover Image
- **Dimensions**: 600px × 350px
- **Image uses: object-fit: cover to ensure no compression**
- Generate a hand-drawn illustration/comic based on the content's main theme, in a style similar to *The New Yorker* editorial cartoons. (The comic reflects the article's meaning)
### 3.2 Title Hierarchy
| Element | Font | Size | Color | Weight | Line Height | Margin |
|---------|------|------|-------|--------|-------------|--------|
| `h1` | Noto Serif SC | 42px | `#000000` | 700 | 1.3 | `margin-bottom: 30px` |
| `h2` | Times New Roman | 26px | `#000000` | 700 | - | `margin: 40px 0 20px` |
| `h3` | Default | 22px | `#2c3e50` | 600 | - | `margin: 30px 0 15px` |
| `h4` | Default | 20px | `#5a6c7d` | 600 | - | `margin: 25px 0 12px` |
### 3.3 Body Text
- **Font size**: `20px`
- **Color**: `#333333`
- **Line height**: `2`
- **Paragraph spacing**: `margin-bottom: 20px`
### 3.4 Special Text Classes
**English title** (`.en-title`)
- Font: Inter
- Size: 18px
- Color: `#888888`
- Weight: 300
**Metadata** (`.metadata`)
- Size: 14px
- Color: `#888888`
---
## 4. Emphasis and Markers
### 4.1 Links (`<a>`)
- Color: `#4a9eff` (blue)
- Default no underline
- Show underline on hover
- Transition effect: `transition: 0.2s ease`
### 4.2 Emphasis (`<em>`)
- Color: `#000000` (black)
- Font style: `normal` (not italic)
- **Usage**: Text that needs emphasis but not highlighting
### 4.3 Bold (`<strong>`)
- **Usage**: Important keywords
### 4.4 Highlight marker (`<mark>`)
- Background color: `#fff59d` (light yellow)
- Text color: `#000000`
- Weight: `bold`
- Bottom border: `2px solid #ff9800` (orange)
- Border radius: `4px`
- Padding: `2px 6px`
---
## 5. Lists and Quotes
### 5.1 Lists (`<ul>`, `<ol>`)
- Font size: `20px`
- Left padding: `20px`
- Bottom margin: `margin-bottom: 20px`
### 5.2 List items (`<li>`)
- Item spacing: `margin-bottom: 8px`
### 5.3 Blockquote (`<blockquote>`)
- Left border: `4px solid #4a9eff` (blue vertical line)
- Left padding: `20px`
- Font style: italic
- Top/bottom margin: `margin: 20px 0`
---
## 6. Code Styles
### 6.1 Code block (`<pre><code>`)
- Font: JetBrains Mono
- Size: `17px`
- Background color: `#f5f5f5`
- Border: `1px solid #e0e0e0`
- Border radius: `6px`
- Padding: `20px`
- Line height: `1.6`
- Horizontal scrolling enabled
### 6.2 Inline code (`<code>`)
- Font: JetBrains Mono
- Size: inherit, slightly smaller
- Background color: `#f5f5f5`
- Padding: `2px 6px`
- Border radius: `4px`
---
## 7. Responsive Design
**Breakpoint**: `650px` and below
| Element | Desktop | Mobile |
|---------|---------|--------|
| Body padding | `20px` | `10px` |
| Body font size | `20px` | `20px` |
| Container width | `600px` | `100%` |
| Container height | `800px` | `auto` (min `80vh`) |
| Content area padding | `50px` | `30px` |
| H1 font size | `42px` | `36px` |
| H2 font size | `26px` | `24px` |
| List font size | `20px` | `20px` |
| Code block font size | `17px` | `15px` |
| Code block padding | `20px` | `15px` |
---
## 8. Output Requirements
1. **Generate complete HTML5 document** with `<!DOCTYPE>`, `<html>`, `<head>`, `<body>` tags
2. **All styles inline in `<style>` tag**, no external CSS file needed
3. **Correctly import Google Fonts**
4. **Ensure semantic HTML structure**
5. **Clean code formatting** with proper indentation
6. **Include viewport meta tag** for responsive support
7. **Set page character encoding to UTF-8**
8. Strictly follow the original content obtained, do not modify, simplify, or delete on your own.
---
## 9. Usage
User will provide article content in `<user_content>` tags, which may contain:
- Cover image
- Titles (h1-h6)
- Paragraph text
- Lists (ordered/unordered)
- Code blocks
- Links
- Emphasis/highlight text
- Blockquotes
Please convert these contents into a beautifully formatted HTML page according to the above specifications.
---
## 10. Special Requirements
Please strictly follow the provided original content, do not modify, delete, or re-polish on your own.
---
+263
View File
@@ -0,0 +1,263 @@
#!/usr/bin/env python3
"""
Cover Image Generation Script - Using Gemini REST API
Usage:
python generate_images.py <prompts.json>
The prompts JSON file should be located in output/<article-folder>/ directory,
and generated images will be saved to output/<article-folder>/images/ directory.
JSON Format:
{
"article": {
"title": "Article Title",
"theme": "Brief description of article theme"
},
"cover": {
"prompt": "Full image generation prompt",
"aspect_ratio": "600x350"
}
}
Or simplified format:
{
"theme": "Article theme description for cover image generation"
}
"""
import os
import sys
import json
import re
import base64
import urllib.request
import urllib.error
from pathlib import Path
from dotenv import load_dotenv
# Load environment variables from .env file in script's parent directory
script_dir = Path(__file__).parent.parent
load_dotenv(script_dir / ".env")
GEMINI_API_KEY = os.getenv("GEMINI_API_KEY")
if not GEMINI_API_KEY:
print("Error: GEMINI_API_KEY environment variable not found")
print("Please create a .env file with GEMINI_API_KEY=your_key")
print("Get your API key from: https://aistudio.google.com/app/apikey")
sys.exit(1)
# Cover image template - New Yorker editorial cartoon style
COVER_TEMPLATE = """Generate a hand-drawn illustration in the style of *The New Yorker* editorial cartoons.
Style specifications:
- Hand-drawn, sketch-like quality
- Minimalist and thoughtful composition
- Subtle humor or irony if appropriate
- Clean lines with cross-hatching for shading
- Limited color palette or black and white
- Editorial cartoon aesthetic
Image dimensions: 600px width × 350px height (landscape)
Theme to illustrate:
{theme}
The illustration should visually capture the essence and main message of this theme in a clever, artistic way typical of New Yorker cartoons."""
def sanitize_prompt(prompt: str) -> str:
"""
Sanitize prompt text to handle newlines, paragraph separators, and special characters.
"""
if not prompt:
return prompt
# Normalize line endings: CRLF -> LF, CR -> LF
text = prompt.replace('\r\n', '\n').replace('\r', '\n')
# Replace paragraph separator (U+2029) and line separator (U+2028) with newlines
text = text.replace('\u2029', '\n\n').replace('\u2028', '\n')
# Split into lines and strip each line
lines = [line.strip() for line in text.split('\n')]
# Collapse multiple empty lines into single empty line
collapsed_lines = []
prev_empty = False
for line in lines:
is_empty = len(line) == 0
if is_empty:
if not prev_empty:
collapsed_lines.append('')
prev_empty = True
else:
collapsed_lines.append(line)
prev_empty = False
# Join with single newlines
text = '\n'.join(collapsed_lines)
# Strip leading/trailing whitespace from the entire text
text = text.strip()
# Collapse multiple spaces into single space (but preserve newlines)
text = re.sub(r'[ \t]+', ' ', text)
return text
def generate_image(prompt: str, filename: str, images_dir: Path) -> str:
"""Generate image using Gemini REST API"""
print(f"Generating: {filename}...")
# Sanitize the prompt to handle newlines and special characters
clean_prompt = sanitize_prompt(prompt)
url = f"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash-exp:generateContent?key={GEMINI_API_KEY}"
payload = {
"contents": [{
"parts": [{"text": clean_prompt}]
}],
"generationConfig": {
"responseModalities": ["TEXT", "IMAGE"]
}
}
headers = {"Content-Type": "application/json"}
try:
req = urllib.request.Request(url, data=json.dumps(payload).encode(), headers=headers)
with urllib.request.urlopen(req, timeout=180) as response:
result = json.loads(response.read().decode())
# Extract image
for part in result.get("candidates", [{}])[0].get("content", {}).get("parts", []):
if "inlineData" in part:
image_data = base64.b64decode(part["inlineData"]["data"])
filepath = images_dir / filename
with open(filepath, "wb") as f:
f.write(image_data)
print(f" Saved: {filepath}")
return str(filepath)
print(f" {filename} generation failed: No image returned")
return None
except urllib.error.HTTPError as e:
error_body = e.read().decode() if e.fp else ""
print(f" {filename} generation failed: HTTP {e.code}")
print(f" Error details: {error_body[:200]}")
return None
except Exception as e:
print(f" {filename} generation failed: {e}")
return None
def load_prompts_json(json_path: Path) -> dict:
"""
Load prompts JSON file.
Supports two formats:
1. Full format: {"article": {...}, "cover": {"prompt": "..."}}
2. Simple format: {"theme": "..."}
Returns:
Dictionary with cover prompt ready to use
"""
with open(json_path, 'r', encoding='utf-8') as f:
data = json.load(f)
# Determine prompt
if "cover" in data and "prompt" in data["cover"]:
# Full prompt provided
prompt = data["cover"]["prompt"]
elif "theme" in data:
# Use template with theme
prompt = COVER_TEMPLATE.format(theme=data["theme"])
elif "article" in data and "theme" in data["article"]:
# Theme in article object
prompt = COVER_TEMPLATE.format(theme=data["article"]["theme"])
else:
raise ValueError("JSON must contain 'cover.prompt' or 'theme' field")
return {
"title": data.get("article", {}).get("title", "Untitled"),
"prompt": prompt
}
def main():
# Check command line arguments
if len(sys.argv) < 2:
print("Usage: python generate_images.py <prompts.json>")
print()
print("The prompts JSON file should be in output/<article-folder>/ directory.")
print("Generated cover image will be saved to output/<article-folder>/images/cover.png")
print()
print("JSON format examples:")
print()
print("Simple format:")
print('''
{
"theme": "The irony of social media bringing people together while isolating them"
}
''')
print("Full format:")
print('''
{
"article": {
"title": "Article Title",
"theme": "Article theme description"
},
"cover": {
"prompt": "Full custom image generation prompt..."
}
}
''')
sys.exit(1)
json_path = Path(sys.argv[1]).resolve()
if not json_path.exists():
print(f"Error: File does not exist: {json_path}")
sys.exit(1)
# Determine output directory (images subdirectory of JSON file's directory)
output_dir = json_path.parent
images_dir = output_dir / "images"
images_dir.mkdir(parents=True, exist_ok=True)
# Load prompts
try:
config = load_prompts_json(json_path)
except Exception as e:
print(f"Error: Failed to load prompts file: {e}")
sys.exit(1)
print("=" * 60)
print("Cover Image Generation")
print(f"Article: {config['title']}")
print(f"Prompts file: {json_path}")
print(f"Output directory: {images_dir}")
print("=" * 60)
# Generate cover image
result = generate_image(config["prompt"], "cover.png", images_dir)
print()
print("=" * 60)
if result:
print("Cover image generated successfully!")
print(f"Location: {result}")
else:
print("Cover image generation failed.")
print("=" * 60)
return 0 if result else 1
if __name__ == "__main__":
sys.exit(main())
+264
View File
@@ -0,0 +1,264 @@
#!/usr/bin/env python3
"""
Screenshot Capture Script for Xiaohongshu Images
Captures sequential screenshots of an HTML page at 3:4 aspect ratio,
ensuring no text is cut off at boundaries.
Usage:
python screenshot.py <html_file_path>
Output:
Screenshots saved to <html_folder>/screenshots/01.png, 02.png, etc.
"""
import os
import sys
import json
from pathlib import Path
try:
from playwright.sync_api import sync_playwright
except ImportError:
print("Error: Playwright is not installed.")
print("Install it with: pip install playwright && playwright install chromium")
sys.exit(1)
# Screenshot dimensions for Xiaohongshu 3:4 ratio
SCREENSHOT_WIDTH = 1080
SCREENSHOT_HEIGHT = 1440 # 3:4 ratio
def find_safe_cut_point(page, y_position: int, search_range: int = 100) -> int:
"""
Find a safe cutting point near y_position where no text is cut.
Searches upward from y_position to find a gap between text elements.
Args:
page: Playwright page object
y_position: Target Y position to cut
search_range: How far to search upward for safe point
Returns:
Safe Y position to cut at
"""
# JavaScript to find text boundaries near the cut point
script = f"""
() => {{
const targetY = {y_position};
const searchRange = {search_range};
// Get all text-containing elements
const textElements = document.querySelectorAll('p, h1, h2, h3, h4, h5, h6, li, span, a, code, pre, blockquote');
let safeCutPoint = targetY;
let minGap = Infinity;
// Find elements that might be cut
for (const el of textElements) {{
const rect = el.getBoundingClientRect();
const elTop = rect.top + window.scrollY;
const elBottom = rect.bottom + window.scrollY;
// Check if this element would be cut at targetY
if (elTop < targetY && elBottom > targetY) {{
// This element would be cut, find gap above it
const gapAbove = targetY - elTop;
if (gapAbove < searchRange && gapAbove < minGap) {{
// Cut above this element instead
safeCutPoint = Math.max(0, elTop - 10);
minGap = gapAbove;
}}
}}
}}
// Also check for line breaks within text blocks
// Find the nearest paragraph boundary
const paragraphs = document.querySelectorAll('p, li, h1, h2, h3, h4, h5, h6');
for (const p of paragraphs) {{
const rect = p.getBoundingClientRect();
const pBottom = rect.bottom + window.scrollY;
// If paragraph ends near our cut point, prefer cutting there
if (pBottom < targetY && targetY - pBottom < searchRange) {{
const gap = targetY - pBottom;
if (gap < minGap) {{
safeCutPoint = pBottom + 5;
minGap = gap;
}}
}}
}}
return Math.max(0, Math.floor(safeCutPoint));
}}
"""
try:
safe_y = page.evaluate(script)
return safe_y if safe_y > 0 else y_position
except Exception as e:
print(f" Warning: Could not find safe cut point: {e}")
return y_position
def capture_screenshots(html_path: Path, output_dir: Path):
"""
Capture sequential screenshots of an HTML page.
Args:
html_path: Path to the HTML file
output_dir: Directory to save screenshots
"""
screenshots_dir = output_dir / "screenshots"
screenshots_dir.mkdir(parents=True, exist_ok=True)
print(f"Opening: {html_path}")
print(f"Screenshots will be saved to: {screenshots_dir}")
print(f"Screenshot size: {SCREENSHOT_WIDTH}x{SCREENSHOT_HEIGHT} (3:4 ratio)")
print()
with sync_playwright() as p:
# Launch browser
browser = p.chromium.launch(headless=True)
# Create page with specific viewport
context = browser.new_context(
viewport={"width": SCREENSHOT_WIDTH, "height": SCREENSHOT_HEIGHT},
device_scale_factor=2 # Retina quality
)
page = context.new_page()
# Navigate to the HTML file
file_url = f"file://{html_path.resolve()}"
page.goto(file_url, wait_until="networkidle")
# Wait for fonts and images to load
page.wait_for_timeout(2000)
# Get total page height
total_height = page.evaluate("() => document.documentElement.scrollHeight")
print(f"Total page height: {total_height}px")
# Calculate number of screenshots needed
current_y = 0
screenshot_index = 1
captured_screenshots = []
while current_y < total_height:
# Calculate the target end position for this screenshot
target_end_y = current_y + SCREENSHOT_HEIGHT
if target_end_y >= total_height:
# Last screenshot - capture whatever remains
actual_height = total_height - current_y
# Scroll to position
page.evaluate(f"window.scrollTo(0, {current_y})")
page.wait_for_timeout(200)
# Take screenshot
filename = f"{screenshot_index:02d}.png"
filepath = screenshots_dir / filename
# For the last screenshot, we might have less than full height
# Add white padding if needed
page.screenshot(
path=str(filepath),
clip={
"x": 0,
"y": 0,
"width": SCREENSHOT_WIDTH,
"height": min(SCREENSHOT_HEIGHT, actual_height + 50) # Small buffer
}
)
print(f" Captured: {filename} (final, {actual_height}px of content)")
captured_screenshots.append(str(filepath))
break
else:
# Find safe cut point to avoid cutting text
safe_end_y = find_safe_cut_point(page, target_end_y)
actual_height = safe_end_y - current_y
# Ensure we make progress even if safe cut point is the same
if actual_height < SCREENSHOT_HEIGHT * 0.5:
actual_height = SCREENSHOT_HEIGHT
safe_end_y = current_y + actual_height
# Scroll to position
page.evaluate(f"window.scrollTo(0, {current_y})")
page.wait_for_timeout(200)
# Take screenshot
filename = f"{screenshot_index:02d}.png"
filepath = screenshots_dir / filename
page.screenshot(
path=str(filepath),
clip={
"x": 0,
"y": 0,
"width": SCREENSHOT_WIDTH,
"height": SCREENSHOT_HEIGHT
}
)
print(f" Captured: {filename} (y: {current_y} to {safe_end_y})")
captured_screenshots.append(str(filepath))
# Move to next section
current_y = safe_end_y
screenshot_index += 1
browser.close()
return captured_screenshots
def main():
if len(sys.argv) < 2:
print("Usage: python screenshot.py <html_file_path>")
print()
print("Captures sequential 3:4 ratio screenshots of an HTML page.")
print("Screenshots are saved to <html_folder>/screenshots/")
sys.exit(1)
html_path = Path(sys.argv[1]).resolve()
if not html_path.exists():
print(f"Error: File does not exist: {html_path}")
sys.exit(1)
if not html_path.suffix.lower() in ['.html', '.htm']:
print(f"Warning: File does not appear to be HTML: {html_path}")
# Output directory is the same as HTML file's directory
output_dir = html_path.parent
print("=" * 60)
print("Xiaohongshu Screenshot Capture")
print("=" * 60)
try:
screenshots = capture_screenshots(html_path, output_dir)
print()
print("=" * 60)
print(f"Screenshot capture complete!")
print(f"Total screenshots: {len(screenshots)}")
print(f"Location: {output_dir / 'screenshots'}")
print("=" * 60)
return 0
except Exception as e:
print(f"Error during screenshot capture: {e}")
import traceback
traceback.print_exc()
return 1
if __name__ == "__main__":
sys.exit(main())