@justin_666/square-couplets-master-skills 1.0.2 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -1
- package/bin/doufang-image.js +34 -0
- package/bin/doufang-init.js +81 -2
- package/bin/doufang-optimize.js +34 -0
- package/bin/doufang-prompt.js +34 -0
- package/constants.ts +240 -0
- package/package.json +10 -3
- package/services/geminiService.ts +236 -0
- package/skills/README.md +40 -1
- package/skills/generate-doufang-image/index.js +202 -0
- package/skills/generate-doufang-prompt/index.js +141 -0
- package/skills/optimize-doufang-prompt/index.js +103 -0
- package/types.ts +70 -0
- package/utils/errorHandler.ts +123 -0
- package/utils/imageUtils.ts +135 -0
- package/utils/retry.ts +41 -0
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { GoogleGenAI, Type } from "@google/genai";
|
|
2
|
+
import {
|
|
3
|
+
DOUFANG_SYSTEM_PROMPT,
|
|
4
|
+
getDoufangSystemPromptWithReference,
|
|
5
|
+
getReferenceImageAnalysisPrompt,
|
|
6
|
+
getSimpleUserInputPrompt,
|
|
7
|
+
getImageGenerationPromptWithReference
|
|
8
|
+
} from "../constants";
|
|
9
|
+
import { processImageDataUrl } from "../utils/imageUtils";
|
|
10
|
+
import { handleApiError } from "../utils/errorHandler";
|
|
11
|
+
import { retryWithBackoff } from "../utils/retry";
|
|
12
|
+
import type { GeminiContentPart } from "../types";
|
|
13
|
+
|
|
14
|
+
const getClient = (apiKey?: string) => {
|
|
15
|
+
// Use user-provided key if available, otherwise fallback to env var
|
|
16
|
+
const userKey = apiKey?.trim();
|
|
17
|
+
const envKey = process.env.API_KEY?.trim();
|
|
18
|
+
const key = userKey || envKey;
|
|
19
|
+
|
|
20
|
+
if (!key) {
|
|
21
|
+
throw new Error("API Key is missing. Please click the Settings icon to configure your Gemini API Key.");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Basic validation: API keys should be non-empty strings
|
|
25
|
+
if (typeof key !== 'string' || key.length === 0) {
|
|
26
|
+
throw new Error("Invalid API Key format. Please check your API Key configuration.");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return new GoogleGenAI({ apiKey: key });
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const generateDoufangPrompt = async (
|
|
33
|
+
userKeyword: string,
|
|
34
|
+
apiKey?: string,
|
|
35
|
+
referenceImageDataUrl?: string | null,
|
|
36
|
+
signal?: AbortSignal
|
|
37
|
+
): Promise<{ blessingPhrase: string; imagePrompt: string }> => {
|
|
38
|
+
return retryWithBackoff(async () => {
|
|
39
|
+
const ai = getClient(apiKey);
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
// Check if request was cancelled
|
|
43
|
+
if (signal?.aborted) {
|
|
44
|
+
throw new Error('Request cancelled');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Prepare content parts
|
|
48
|
+
const parts: GeminiContentPart[] = [];
|
|
49
|
+
|
|
50
|
+
// Add reference image if provided - image should come first
|
|
51
|
+
if (referenceImageDataUrl) {
|
|
52
|
+
const imageData = processImageDataUrl(referenceImageDataUrl);
|
|
53
|
+
if (imageData) {
|
|
54
|
+
parts.push({
|
|
55
|
+
inlineData: {
|
|
56
|
+
mimeType: imageData.mimeType,
|
|
57
|
+
data: imageData.base64Data
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
} else {
|
|
61
|
+
// Fallback: try to use as-is (may fail, but attempt anyway)
|
|
62
|
+
console.warn('Reference image format may be incorrect, attempting to use as-is');
|
|
63
|
+
parts.push({
|
|
64
|
+
inlineData: {
|
|
65
|
+
mimeType: 'image/jpeg', // Default to JPEG
|
|
66
|
+
data: referenceImageDataUrl.replace(/^data:image\/[^;]+;base64,/, '')
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Add text instruction with reference image context
|
|
72
|
+
parts.push({
|
|
73
|
+
text: getReferenceImageAnalysisPrompt(userKeyword)
|
|
74
|
+
});
|
|
75
|
+
} else {
|
|
76
|
+
// No reference image, use simple text
|
|
77
|
+
parts.push({ text: getSimpleUserInputPrompt(userKeyword) });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Get system instruction based on whether reference image is provided
|
|
81
|
+
const systemInstruction = referenceImageDataUrl
|
|
82
|
+
? getDoufangSystemPromptWithReference()
|
|
83
|
+
: DOUFANG_SYSTEM_PROMPT;
|
|
84
|
+
|
|
85
|
+
const response = await ai.models.generateContent({
|
|
86
|
+
model: 'gemini-3-flash-preview',
|
|
87
|
+
contents: {
|
|
88
|
+
parts: parts
|
|
89
|
+
},
|
|
90
|
+
config: {
|
|
91
|
+
systemInstruction: systemInstruction,
|
|
92
|
+
responseMimeType: "application/json",
|
|
93
|
+
responseSchema: {
|
|
94
|
+
type: Type.OBJECT,
|
|
95
|
+
properties: {
|
|
96
|
+
blessingPhrase: { type: Type.STRING },
|
|
97
|
+
imagePrompt: { type: Type.STRING }
|
|
98
|
+
},
|
|
99
|
+
required: ["blessingPhrase", "imagePrompt"]
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const text = response.text;
|
|
105
|
+
if (!text) {
|
|
106
|
+
throw new Error("No response from Gemini");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Check if request was cancelled before parsing
|
|
110
|
+
if (signal?.aborted) {
|
|
111
|
+
throw new Error('Request cancelled');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return JSON.parse(text);
|
|
115
|
+
} catch (e: unknown) {
|
|
116
|
+
if (signal?.aborted) {
|
|
117
|
+
throw new Error('Request cancelled');
|
|
118
|
+
}
|
|
119
|
+
throw handleApiError(e, 'generateDoufangPrompt');
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
export const generateDoufangImage = async (
|
|
125
|
+
prompt: string,
|
|
126
|
+
apiKey?: string,
|
|
127
|
+
model: string = 'gemini-2.5-flash-image',
|
|
128
|
+
imageSize: '1K' | '2K' | '4K' = '1K',
|
|
129
|
+
referenceImageDataUrl?: string | null,
|
|
130
|
+
signal?: AbortSignal
|
|
131
|
+
): Promise<string> => {
|
|
132
|
+
return retryWithBackoff(async () => {
|
|
133
|
+
const ai = getClient(apiKey);
|
|
134
|
+
|
|
135
|
+
// Check if request was cancelled
|
|
136
|
+
if (signal?.aborted) {
|
|
137
|
+
throw new Error('Request cancelled');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
let config: Record<string, unknown> = {};
|
|
141
|
+
|
|
142
|
+
// Different models have different support for imageConfig
|
|
143
|
+
// Flash model does NOT support imageConfig parameter - it will cause 400 errors
|
|
144
|
+
// Only Pro model supports imageConfig with custom sizes
|
|
145
|
+
if (model === 'gemini-3-pro-image-preview') {
|
|
146
|
+
// Pro model supports all sizes (1K, 2K, 4K)
|
|
147
|
+
config = {
|
|
148
|
+
imageConfig: {
|
|
149
|
+
aspectRatio: "1:1",
|
|
150
|
+
imageSize: imageSize
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
// For Flash model: Do NOT set imageConfig at all
|
|
155
|
+
// Flash model only supports default 1K (1024x1024) resolution
|
|
156
|
+
// Setting imageConfig will cause 400 INVALID_ARGUMENT error
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
// Prepare content parts
|
|
160
|
+
const parts: GeminiContentPart[] = [];
|
|
161
|
+
|
|
162
|
+
// Add reference image if provided - image should come first
|
|
163
|
+
// Note: The prompt already contains style guidance from the reference image
|
|
164
|
+
// (generated in generateDoufangPrompt), so we just need to provide the image
|
|
165
|
+
// as additional visual reference for the image generation model
|
|
166
|
+
if (referenceImageDataUrl) {
|
|
167
|
+
const imageData = processImageDataUrl(referenceImageDataUrl);
|
|
168
|
+
if (imageData) {
|
|
169
|
+
parts.push({
|
|
170
|
+
inlineData: {
|
|
171
|
+
mimeType: imageData.mimeType,
|
|
172
|
+
data: imageData.base64Data
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
} else {
|
|
176
|
+
// Fallback: try to use as-is (may fail, but attempt anyway)
|
|
177
|
+
console.warn('Reference image format may be incorrect, attempting to use as-is');
|
|
178
|
+
parts.push({
|
|
179
|
+
inlineData: {
|
|
180
|
+
mimeType: 'image/jpeg', // Default to JPEG
|
|
181
|
+
data: referenceImageDataUrl.replace(/^data:image\/[^;]+;base64,/, '')
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Add prompt with reference image context
|
|
187
|
+
// The prompt already includes style guidance, so we just reinforce it
|
|
188
|
+
parts.push({
|
|
189
|
+
text: getImageGenerationPromptWithReference(prompt)
|
|
190
|
+
});
|
|
191
|
+
} else {
|
|
192
|
+
// No reference image, use original prompt
|
|
193
|
+
parts.push({ text: prompt });
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const response = await ai.models.generateContent({
|
|
197
|
+
model: model,
|
|
198
|
+
contents: {
|
|
199
|
+
parts: parts
|
|
200
|
+
},
|
|
201
|
+
config: Object.keys(config).length > 0 ? config : undefined
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
// Check if request was cancelled before processing response
|
|
205
|
+
if (signal?.aborted) {
|
|
206
|
+
throw new Error('Request cancelled');
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Extract image
|
|
210
|
+
for (const part of response.candidates?.[0]?.content?.parts || []) {
|
|
211
|
+
if (part.inlineData) {
|
|
212
|
+
return `data:${part.inlineData.mimeType};base64,${part.inlineData.data}`;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
throw new Error("No image generated in the response.");
|
|
217
|
+
} catch (e: unknown) {
|
|
218
|
+
if (signal?.aborted) {
|
|
219
|
+
throw new Error('Request cancelled');
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const error = handleApiError(e, 'generateDoufangImage');
|
|
223
|
+
|
|
224
|
+
// Add specific context for image size errors
|
|
225
|
+
if (error.code === 'INVALID_REQUEST') {
|
|
226
|
+
if (model === 'gemini-2.5-flash-image') {
|
|
227
|
+
error.userMessage = 'Flash 模型不支援自訂圖片大小設定,僅支援預設 1K (1024×1024) 解析度。如需更高解析度,請使用 Pro 模型。';
|
|
228
|
+
} else if (imageSize === '4K') {
|
|
229
|
+
error.userMessage = '4K 解析度可能不被此模型或您的 API 方案支援,請嘗試 2K 或 1K。';
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
throw error;
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
};
|
package/skills/README.md
CHANGED
|
@@ -23,6 +23,37 @@ doufang-skills show generate-doufang-prompt
|
|
|
23
23
|
doufang-skills path generate-doufang-image
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
+
### 使用 CLI 命令執行 Skills
|
|
27
|
+
|
|
28
|
+
安裝後,您可以直接使用 CLI 命令執行 skills:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
# 生成 prompt
|
|
32
|
+
doufang-prompt "財富"
|
|
33
|
+
doufang-prompt "健康" images/reference.png
|
|
34
|
+
|
|
35
|
+
# 生成圖片
|
|
36
|
+
doufang-image "A diamond-shaped Doufang..." gemini-3-pro-image-preview 2K
|
|
37
|
+
doufang-image "..." gemini-3-pro-image-preview 2K images/ref.png output/my-doufang.png
|
|
38
|
+
|
|
39
|
+
# 優化 prompt
|
|
40
|
+
doufang-optimize "A diamond-shaped Doufang with wide white margins..."
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### 直接執行 Skill 腳本
|
|
44
|
+
|
|
45
|
+
您也可以直接執行 skill 腳本:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
# 從專案根目錄
|
|
49
|
+
node skills/generate-doufang-prompt/index.js "財富"
|
|
50
|
+
node skills/generate-doufang-image/index.js "..." gemini-3-pro-image-preview 2K
|
|
51
|
+
node skills/optimize-doufang-prompt/index.js "..."
|
|
52
|
+
|
|
53
|
+
# 從 npm 包(如果已安裝)
|
|
54
|
+
node node_modules/@justin_666/square-couplets-master-skills/skills/generate-doufang-prompt/index.js "財富"
|
|
55
|
+
```
|
|
56
|
+
|
|
26
57
|
### 方式 2:從 GitHub 克隆
|
|
27
58
|
|
|
28
59
|
```bash
|
|
@@ -68,12 +99,20 @@ npm install @justin_666/square-couplets-master-skills
|
|
|
68
99
|
```
|
|
69
100
|
|
|
70
101
|
4. **使用 Slash Command**:
|
|
71
|
-
|
|
102
|
+
|
|
103
|
+
**在 Cursor 中**:
|
|
104
|
+
- 輸入 `/` 會自動顯示 `/doufang` 選項(已自動註冊)
|
|
105
|
+
- 選擇 `/doufang` 後輸入您的請求
|
|
106
|
+
- 或直接輸入:`/doufang Generate a prompt for wealth theme`
|
|
107
|
+
|
|
108
|
+
**在 Windsurf / Antigravity 中**:
|
|
72
109
|
```
|
|
73
110
|
/doufang Generate a prompt for wealth theme
|
|
74
111
|
/doufang Create a 2K image using Gemini 3 Pro
|
|
75
112
|
/doufang Optimize this prompt to reduce white space
|
|
76
113
|
```
|
|
114
|
+
|
|
115
|
+
**注意**:Cursor 會自動識別 `/doufang` 命令並使用 CLI 工具執行,無需手動編寫代碼。
|
|
77
116
|
|
|
78
117
|
### 手動設置
|
|
79
118
|
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Executable script for generate-doufang-image skill
|
|
5
|
+
* Can be called directly by agents or users
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { fileURLToPath } from 'url';
|
|
9
|
+
import { dirname, join, resolve } from 'path';
|
|
10
|
+
import { readFileSync, writeFileSync, existsSync, statSync, mkdirSync } from 'fs';
|
|
11
|
+
import { config } from 'dotenv';
|
|
12
|
+
|
|
13
|
+
// Resolve project root and service path
|
|
14
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
15
|
+
const __dirname = dirname(__filename);
|
|
16
|
+
const skillDir = resolve(__dirname);
|
|
17
|
+
const projectRoot = resolve(skillDir, '../..');
|
|
18
|
+
|
|
19
|
+
// Try to find services directory
|
|
20
|
+
function findServicesPath() {
|
|
21
|
+
const possiblePaths = [
|
|
22
|
+
join(projectRoot, 'services'),
|
|
23
|
+
join(projectRoot, 'node_modules', '@justin_666', 'square-couplets-master-skills', 'services'),
|
|
24
|
+
join(process.cwd(), 'services'),
|
|
25
|
+
join(process.cwd(), 'node_modules', '@justin_666', 'square-couplets-master-skills', 'services'),
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
for (const path of possiblePaths) {
|
|
29
|
+
try {
|
|
30
|
+
if (statSync(path).isDirectory()) {
|
|
31
|
+
return path;
|
|
32
|
+
}
|
|
33
|
+
} catch (e) {
|
|
34
|
+
// Path doesn't exist, try next
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Load environment variables
|
|
41
|
+
const envLocalPath = join(projectRoot, '.env.local');
|
|
42
|
+
const envPath = join(projectRoot, '.env');
|
|
43
|
+
|
|
44
|
+
if (existsSync(envLocalPath)) {
|
|
45
|
+
config({ path: envLocalPath });
|
|
46
|
+
} else if (existsSync(envPath)) {
|
|
47
|
+
config({ path: envPath });
|
|
48
|
+
} else {
|
|
49
|
+
config();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function main() {
|
|
53
|
+
try {
|
|
54
|
+
// Parse command line arguments
|
|
55
|
+
const args = process.argv.slice(2);
|
|
56
|
+
const prompt = args[0];
|
|
57
|
+
const model = args[1] || 'gemini-2.5-flash-image';
|
|
58
|
+
const imageSize = args[2] || '1K';
|
|
59
|
+
const referenceImagePath = args[3]; // Optional reference image path
|
|
60
|
+
const outputPath = args[4]; // Optional output path
|
|
61
|
+
|
|
62
|
+
if (!prompt) {
|
|
63
|
+
console.error('❌ Error: Prompt is required');
|
|
64
|
+
console.log('\nUsage:');
|
|
65
|
+
console.log(' node skills/generate-doufang-image/index.js <prompt> [model] [size] [reference-image] [output-path]');
|
|
66
|
+
console.log('\nParameters:');
|
|
67
|
+
console.log(' prompt - Image generation prompt (required)');
|
|
68
|
+
console.log(' model - Model to use: gemini-2.5-flash-image (default) or gemini-3-pro-image-preview');
|
|
69
|
+
console.log(' size - Image size: 1K (default), 2K, or 4K (Pro model only)');
|
|
70
|
+
console.log(' reference-image - Optional reference image path');
|
|
71
|
+
console.log(' output-path - Optional output file path (default: output/doufang-{timestamp}.png)');
|
|
72
|
+
console.log('\nExample:');
|
|
73
|
+
console.log(' node skills/generate-doufang-image/index.js "A diamond-shaped Doufang..."');
|
|
74
|
+
console.log(' node skills/generate-doufang-image/index.js "..." gemini-3-pro-image-preview 2K');
|
|
75
|
+
console.log(' node skills/generate-doufang-image/index.js "..." gemini-3-pro-image-preview 2K images/ref.png output/my-doufang.png');
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Validate image size
|
|
80
|
+
if (!['1K', '2K', '4K'].includes(imageSize)) {
|
|
81
|
+
console.error('❌ Error: Invalid image size. Must be 1K, 2K, or 4K');
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Validate model and size combination
|
|
86
|
+
if (model === 'gemini-2.5-flash-image' && imageSize !== '1K') {
|
|
87
|
+
console.error('❌ Error: Flash model only supports 1K resolution');
|
|
88
|
+
console.log('💡 Use Pro model (gemini-3-pro-image-preview) for 2K/4K');
|
|
89
|
+
process.exit(1);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Get API key
|
|
93
|
+
const apiKey = process.env.GEMINI_API_KEY || process.env.API_KEY || process.env.GOOGLE_GENAI_API_KEY;
|
|
94
|
+
|
|
95
|
+
if (!apiKey) {
|
|
96
|
+
console.error('❌ Error: API Key is missing');
|
|
97
|
+
console.log('💡 Set GEMINI_API_KEY in .env file or environment variable');
|
|
98
|
+
process.exit(1);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Try to import service function
|
|
102
|
+
const servicesPath = findServicesPath();
|
|
103
|
+
if (!servicesPath) {
|
|
104
|
+
console.error('❌ Error: Cannot find services directory');
|
|
105
|
+
console.log('💡 Make sure you are running from the project root or have installed the package');
|
|
106
|
+
process.exit(1);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Dynamic import of service (support both .ts and .js)
|
|
110
|
+
let serviceModule;
|
|
111
|
+
try {
|
|
112
|
+
// Try .js first (for npm package)
|
|
113
|
+
serviceModule = await import(`file://${join(servicesPath, 'geminiService.js')}`);
|
|
114
|
+
} catch (e) {
|
|
115
|
+
try {
|
|
116
|
+
// Try .ts (for development)
|
|
117
|
+
serviceModule = await import(`file://${join(servicesPath, 'geminiService.ts')}`);
|
|
118
|
+
} catch (e2) {
|
|
119
|
+
console.error('❌ Error: Cannot import service module');
|
|
120
|
+
console.error(' Tried:', join(servicesPath, 'geminiService.js'));
|
|
121
|
+
console.error(' Tried:', join(servicesPath, 'geminiService.ts'));
|
|
122
|
+
process.exit(1);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
const { generateDoufangImage } = serviceModule;
|
|
126
|
+
|
|
127
|
+
// Load reference image if provided
|
|
128
|
+
let referenceImageDataUrl = null;
|
|
129
|
+
if (referenceImagePath) {
|
|
130
|
+
const fullPath = resolve(process.cwd(), referenceImagePath);
|
|
131
|
+
if (!existsSync(fullPath)) {
|
|
132
|
+
console.error(`❌ Error: Reference image not found: ${fullPath}`);
|
|
133
|
+
process.exit(1);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const imageBuffer = readFileSync(fullPath);
|
|
137
|
+
const base64 = imageBuffer.toString('base64');
|
|
138
|
+
const ext = referenceImagePath.split('.').pop()?.toLowerCase();
|
|
139
|
+
const mimeType = ext === 'jpg' || ext === 'jpeg' ? 'image/jpeg' : 'image/png';
|
|
140
|
+
referenceImageDataUrl = `data:${mimeType};base64,${base64}`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Generate image
|
|
144
|
+
console.log(`🖼️ Generating image...`);
|
|
145
|
+
console.log(` Model: ${model}`);
|
|
146
|
+
console.log(` Size: ${imageSize}`);
|
|
147
|
+
if (referenceImagePath) {
|
|
148
|
+
console.log(` Reference: ${referenceImagePath}`);
|
|
149
|
+
}
|
|
150
|
+
console.log(' This may take a while, please wait...\n');
|
|
151
|
+
|
|
152
|
+
const imageDataUrl = await generateDoufangImage(
|
|
153
|
+
prompt,
|
|
154
|
+
apiKey,
|
|
155
|
+
model,
|
|
156
|
+
imageSize,
|
|
157
|
+
referenceImageDataUrl
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
// Extract base64 data
|
|
161
|
+
const base64Data = imageDataUrl.replace(/^data:image\/\w+;base64,/, '');
|
|
162
|
+
const buffer = Buffer.from(base64Data, 'base64');
|
|
163
|
+
|
|
164
|
+
// Determine output path
|
|
165
|
+
let finalOutputPath = outputPath;
|
|
166
|
+
if (!finalOutputPath) {
|
|
167
|
+
const outputDir = join(process.cwd(), 'output');
|
|
168
|
+
if (!existsSync(outputDir)) {
|
|
169
|
+
mkdirSync(outputDir, { recursive: true });
|
|
170
|
+
}
|
|
171
|
+
const timestamp = Date.now();
|
|
172
|
+
finalOutputPath = join(outputDir, `doufang-${timestamp}.png`);
|
|
173
|
+
} else {
|
|
174
|
+
finalOutputPath = resolve(process.cwd(), finalOutputPath);
|
|
175
|
+
// Ensure output directory exists
|
|
176
|
+
const outputDir = dirname(finalOutputPath);
|
|
177
|
+
if (!existsSync(outputDir)) {
|
|
178
|
+
mkdirSync(outputDir, { recursive: true });
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Save image
|
|
183
|
+
writeFileSync(finalOutputPath, buffer);
|
|
184
|
+
|
|
185
|
+
console.log('✅ Image generated successfully!');
|
|
186
|
+
console.log(`📁 Saved to: ${finalOutputPath}`);
|
|
187
|
+
console.log(`📊 File size: ${(buffer.length / 1024 / 1024).toFixed(2)} MB`);
|
|
188
|
+
|
|
189
|
+
// Also output data URL for programmatic use
|
|
190
|
+
console.log('\n📋 Image data URL (for programmatic use):');
|
|
191
|
+
console.log(imageDataUrl.substring(0, 100) + '...');
|
|
192
|
+
|
|
193
|
+
} catch (error) {
|
|
194
|
+
console.error('❌ Error:', error.message);
|
|
195
|
+
if (error.stack) {
|
|
196
|
+
console.error(error.stack);
|
|
197
|
+
}
|
|
198
|
+
process.exit(1);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
main();
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Executable script for generate-doufang-prompt skill
|
|
5
|
+
* Can be called directly by agents or users
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { fileURLToPath } from 'url';
|
|
9
|
+
import { dirname, join, resolve } from 'path';
|
|
10
|
+
import { readFileSync, existsSync, statSync } from 'fs';
|
|
11
|
+
import { config } from 'dotenv';
|
|
12
|
+
|
|
13
|
+
// Resolve project root and service path
|
|
14
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
15
|
+
const __dirname = dirname(__filename);
|
|
16
|
+
const skillDir = resolve(__dirname);
|
|
17
|
+
const projectRoot = resolve(skillDir, '../..');
|
|
18
|
+
|
|
19
|
+
// Try to find services directory
|
|
20
|
+
function findServicesPath() {
|
|
21
|
+
const possiblePaths = [
|
|
22
|
+
join(projectRoot, 'services'),
|
|
23
|
+
join(projectRoot, 'node_modules', '@justin_666', 'square-couplets-master-skills', 'services'),
|
|
24
|
+
join(process.cwd(), 'services'),
|
|
25
|
+
join(process.cwd(), 'node_modules', '@justin_666', 'square-couplets-master-skills', 'services'),
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
for (const path of possiblePaths) {
|
|
29
|
+
try {
|
|
30
|
+
if (statSync(path).isDirectory()) {
|
|
31
|
+
return path;
|
|
32
|
+
}
|
|
33
|
+
} catch (e) {
|
|
34
|
+
// Path doesn't exist, try next
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Load environment variables
|
|
41
|
+
const envLocalPath = join(projectRoot, '.env.local');
|
|
42
|
+
const envPath = join(projectRoot, '.env');
|
|
43
|
+
|
|
44
|
+
if (existsSync(envLocalPath)) {
|
|
45
|
+
config({ path: envLocalPath });
|
|
46
|
+
} else if (existsSync(envPath)) {
|
|
47
|
+
config({ path: envPath });
|
|
48
|
+
} else {
|
|
49
|
+
// Try current working directory
|
|
50
|
+
config();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function main() {
|
|
54
|
+
try {
|
|
55
|
+
// Parse command line arguments
|
|
56
|
+
const args = process.argv.slice(2);
|
|
57
|
+
const keyword = args[0];
|
|
58
|
+
const referenceImagePath = args[1]; // Optional reference image path
|
|
59
|
+
|
|
60
|
+
if (!keyword) {
|
|
61
|
+
console.error('❌ Error: Keyword is required');
|
|
62
|
+
console.log('\nUsage:');
|
|
63
|
+
console.log(' node skills/generate-doufang-prompt/index.js <keyword> [reference-image-path]');
|
|
64
|
+
console.log('\nExample:');
|
|
65
|
+
console.log(' node skills/generate-doufang-prompt/index.js "財富"');
|
|
66
|
+
console.log(' node skills/generate-doufang-prompt/index.js "健康" images/reference.png');
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Get API key
|
|
71
|
+
const apiKey = process.env.GEMINI_API_KEY || process.env.API_KEY || process.env.GOOGLE_GENAI_API_KEY;
|
|
72
|
+
|
|
73
|
+
if (!apiKey) {
|
|
74
|
+
console.error('❌ Error: API Key is missing');
|
|
75
|
+
console.log('💡 Set GEMINI_API_KEY in .env file or environment variable');
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Try to import service function
|
|
80
|
+
const servicesPath = findServicesPath();
|
|
81
|
+
if (!servicesPath) {
|
|
82
|
+
console.error('❌ Error: Cannot find services directory');
|
|
83
|
+
console.log('💡 Make sure you are running from the project root or have installed the package');
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Dynamic import of service (support both .ts and .js)
|
|
88
|
+
let serviceModule;
|
|
89
|
+
try {
|
|
90
|
+
// Try .js first (for npm package)
|
|
91
|
+
serviceModule = await import(`file://${join(servicesPath, 'geminiService.js')}`);
|
|
92
|
+
} catch (e) {
|
|
93
|
+
try {
|
|
94
|
+
// Try .ts (for development)
|
|
95
|
+
serviceModule = await import(`file://${join(servicesPath, 'geminiService.ts')}`);
|
|
96
|
+
} catch (e2) {
|
|
97
|
+
console.error('❌ Error: Cannot import service module');
|
|
98
|
+
console.error(' Tried:', join(servicesPath, 'geminiService.js'));
|
|
99
|
+
console.error(' Tried:', join(servicesPath, 'geminiService.ts'));
|
|
100
|
+
process.exit(1);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const { generateDoufangPrompt } = serviceModule;
|
|
104
|
+
|
|
105
|
+
// Load reference image if provided
|
|
106
|
+
let referenceImageDataUrl = null;
|
|
107
|
+
if (referenceImagePath) {
|
|
108
|
+
const fullPath = resolve(process.cwd(), referenceImagePath);
|
|
109
|
+
if (!existsSync(fullPath)) {
|
|
110
|
+
console.error(`❌ Error: Reference image not found: ${fullPath}`);
|
|
111
|
+
process.exit(1);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const imageBuffer = readFileSync(fullPath);
|
|
115
|
+
const base64 = imageBuffer.toString('base64');
|
|
116
|
+
const ext = referenceImagePath.split('.').pop()?.toLowerCase();
|
|
117
|
+
const mimeType = ext === 'jpg' || ext === 'jpeg' ? 'image/jpeg' : 'image/png';
|
|
118
|
+
referenceImageDataUrl = `data:${mimeType};base64,${base64}`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Generate prompt
|
|
122
|
+
console.log(`📝 Generating prompt for keyword: "${keyword}"`);
|
|
123
|
+
if (referenceImagePath) {
|
|
124
|
+
console.log(`🖼️ Using reference image: ${referenceImagePath}`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const result = await generateDoufangPrompt(keyword, apiKey, referenceImageDataUrl);
|
|
128
|
+
|
|
129
|
+
// Output as JSON
|
|
130
|
+
console.log(JSON.stringify(result, null, 2));
|
|
131
|
+
|
|
132
|
+
} catch (error) {
|
|
133
|
+
console.error('❌ Error:', error.message);
|
|
134
|
+
if (error.stack) {
|
|
135
|
+
console.error(error.stack);
|
|
136
|
+
}
|
|
137
|
+
process.exit(1);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
main();
|