@justin_666/square-couplets-master-skills 1.0.3 → 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.
@@ -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
- 在 Cursor / Windsurf / Antigravity 的聊天中輸入:
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();