@justin_666/square-couplets-master-skills 1.0.19 → 1.1.0
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/dist/constants.js +6 -6
- package/package.json +1 -1
- package/skills/generate-doufang-image/index.js +69 -158
- package/skills/generate-doufang-prompt/SKILL.md +5 -5
- package/skills/generate-doufang-prompt/index.js +47 -172
- package/skills/optimize-doufang-prompt/SKILL.md +9 -9
- package/skills/optimize-doufang-prompt/index.js +127 -46
- package/skills/shared/utils.js +247 -0
package/dist/constants.js
CHANGED
|
@@ -46,7 +46,7 @@ Lighting: soft studio lighting, gentle glow on gold details, museum-quality artw
|
|
|
46
46
|
Composition:
|
|
47
47
|
The diamond-shaped Doufang fills the majority of the 1:1 frame, centered with minimal elegant margins (approximately 2-5% of frame width, just enough to prevent edge cropping).
|
|
48
48
|
The entire artwork is fully visible inside the frame, not touching any edge, not cropped, not cut off.
|
|
49
|
-
The Doufang should occupy 95% of the image area, maximizing visual impact.
|
|
49
|
+
The Doufang should occupy 90 - 95% of the image area, maximizing visual impact.
|
|
50
50
|
Clean background, symmetrical, perfectly framed, suitable for printing and hanging on wall.
|
|
51
51
|
|
|
52
52
|
Quality: ultra high detail, 8k, masterpiece, professional artwork, 1:1 aspect ratio.
|
|
@@ -54,7 +54,7 @@ Quality: ultra high detail, 8k, masterpiece, professional artwork, 1:1 aspect ra
|
|
|
54
54
|
Framing requirements:
|
|
55
55
|
- The entire diamond-shaped Doufang must be fully visible inside the image.
|
|
56
56
|
- No part of the artwork is cut off, cropped, out of frame, or touching the image borders.
|
|
57
|
-
- Minimal margins - the Doufang should fill most of the frame ( 95% of image area).
|
|
57
|
+
- Minimal margins - the Doufang should fill most of the frame ( 90 - 95% of image area).
|
|
58
58
|
|
|
59
59
|
Text requirements:
|
|
60
60
|
- The Chinese characters must be clear, correct, readable.
|
|
@@ -144,7 +144,7 @@ Your generated "imagePrompt" must follow this logic:
|
|
|
144
144
|
1. **Framing**:
|
|
145
145
|
- The entire diamond Doufang must be fully contained within the 1:1 frame.
|
|
146
146
|
- Minimal, elegant margins - just enough to prevent edge cropping (approximately 2-5% of frame width).
|
|
147
|
-
- The Doufang should fill most of the frame ( 95% of the image area).
|
|
147
|
+
- The Doufang should fill most of the frame ( 90 - 95% of the image area).
|
|
148
148
|
- No cropping, no touching edges, no cut-off.
|
|
149
149
|
2. **Text Quality**:
|
|
150
150
|
- Calligraphy must be clear, professional, and correctly written.
|
|
@@ -157,7 +157,7 @@ Your generated "imagePrompt" must follow this logic:
|
|
|
157
157
|
Composition:
|
|
158
158
|
The diamond-shaped Doufang fills the majority of the 1:1 frame, centered with minimal elegant margins (just enough to prevent edge cropping, approximately 2-5% of frame width).
|
|
159
159
|
The entire artwork is fully visible inside the frame, not touching any edge, not cropped, not cut off.
|
|
160
|
-
The Doufang should occupy 95% of the image area, maximizing visual impact.
|
|
160
|
+
The Doufang should occupy 90 - 95% of the image area, maximizing visual impact.
|
|
161
161
|
Clean background, symmetrical, perfectly framed, suitable for printing and hanging on wall.
|
|
162
162
|
|
|
163
163
|
Quality: ultra high detail, 8k, masterpiece, professional artwork, 1:1 aspect ratio.
|
|
@@ -165,7 +165,7 @@ Quality: ultra high detail, 8k, masterpiece, professional artwork, 1:1 aspect ra
|
|
|
165
165
|
Framing requirements:
|
|
166
166
|
- The entire diamond-shaped Doufang must be fully visible inside the image.
|
|
167
167
|
- No part of the artwork is cut off, cropped, out of frame, or touching the image borders.
|
|
168
|
-
- Minimal margins - the Doufang should fill most of the frame ( 95% of image area).
|
|
168
|
+
- Minimal margins - the Doufang should fill most of the frame ( 90 - 95% of image area).
|
|
169
169
|
|
|
170
170
|
Text requirements:
|
|
171
171
|
- The Chinese characters must be clear, correct, readable.
|
|
@@ -228,7 +228,7 @@ export const getSimpleUserInputPrompt = (userKeyword) => {
|
|
|
228
228
|
export const getImageGenerationPromptWithReference = (basePrompt) => {
|
|
229
229
|
return `${basePrompt}
|
|
230
230
|
|
|
231
|
-
IMPORTANT COMPOSITION NOTE: The diamond-shaped Doufang should fill 95% of the frame with minimal margins (2-5% of frame width). Avoid excessive white space or wide margins. Maximize the visual impact by making the Doufang artwork occupy most of the image area.
|
|
231
|
+
IMPORTANT COMPOSITION NOTE: The diamond-shaped Doufang should fill 90 - 95% of the frame with minimal margins (2-5% of frame width). Avoid excessive white space or wide margins. Maximize the visual impact by making the Doufang artwork occupy most of the image area.
|
|
232
232
|
|
|
233
233
|
Note: The reference image provided above should be used as a visual style guide. Follow the style, color palette, and artistic approach described in the prompt, which was generated based on analysis of this reference image.`;
|
|
234
234
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@justin_666/square-couplets-master-skills",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Claude Agent Skills for generating Chinese New Year Doufang (diamond-shaped couplet) artwork using Google Gemini AI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "bin/doufang-skills.js",
|
|
@@ -5,105 +5,35 @@
|
|
|
5
5
|
* Can be called directly by agents or users
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { fileURLToPath } from 'url';
|
|
9
8
|
import { dirname, join, resolve } from 'path';
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
|
|
9
|
+
import { writeFileSync, existsSync, mkdirSync } from 'fs';
|
|
10
|
+
import {
|
|
11
|
+
getProjectRoot,
|
|
12
|
+
loadEnvironmentVariables,
|
|
13
|
+
findServicesPath,
|
|
14
|
+
getApiKey,
|
|
15
|
+
showVersion,
|
|
16
|
+
loadReferenceImage,
|
|
17
|
+
importServiceModule,
|
|
18
|
+
createProgressSpinner
|
|
19
|
+
} from '../shared/utils.js';
|
|
20
|
+
|
|
21
|
+
// Initialize
|
|
22
|
+
const projectRoot = getProjectRoot(import.meta.url);
|
|
13
23
|
|
|
14
|
-
|
|
15
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
16
|
-
const __dirname = dirname(__filename);
|
|
17
|
-
const skillDir = resolve(__dirname);
|
|
18
|
-
const projectRoot = resolve(skillDir, '../..');
|
|
19
|
-
|
|
20
|
-
// Try to find services directory
|
|
21
|
-
function findServicesPath() {
|
|
22
|
-
// Get npm global prefix to find globally installed packages
|
|
23
|
-
let globalPrefix = null;
|
|
24
|
-
try {
|
|
25
|
-
globalPrefix = execSync('npm config get prefix', { encoding: 'utf-8' }).trim();
|
|
26
|
-
} catch (e) {
|
|
27
|
-
// Ignore error, try other methods
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// Try to resolve package location using createRequire (works in ES modules)
|
|
31
|
-
let packageRoot = null;
|
|
32
|
-
try {
|
|
33
|
-
const require = createRequire(import.meta.url);
|
|
34
|
-
const packageJsonPath = require.resolve('@justin_666/square-couplets-master-skills/package.json');
|
|
35
|
-
packageRoot = dirname(packageJsonPath);
|
|
36
|
-
} catch (e) {
|
|
37
|
-
// Package not found via require.resolve, will try other paths
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const possiblePaths = [
|
|
41
|
-
// Global npm package - dist directory (compiled JS)
|
|
42
|
-
...(globalPrefix ? [
|
|
43
|
-
join(globalPrefix, 'lib', 'node_modules', '@justin_666', 'square-couplets-master-skills', 'dist', 'services'),
|
|
44
|
-
join(globalPrefix, 'node_modules', '@justin_666', 'square-couplets-master-skills', 'dist', 'services'),
|
|
45
|
-
] : []),
|
|
46
|
-
// From resolved package root - dist directory
|
|
47
|
-
...(packageRoot ? [
|
|
48
|
-
join(packageRoot, 'dist', 'services'),
|
|
49
|
-
join(packageRoot, 'services'), // fallback to source
|
|
50
|
-
] : []),
|
|
51
|
-
// Local project root - dist directory
|
|
52
|
-
join(projectRoot, 'dist', 'services'),
|
|
53
|
-
join(projectRoot, 'services'), // fallback to source
|
|
54
|
-
// Local node_modules - dist directory
|
|
55
|
-
join(projectRoot, 'node_modules', '@justin_666', 'square-couplets-master-skills', 'dist', 'services'),
|
|
56
|
-
// Current working directory - dist directory
|
|
57
|
-
join(process.cwd(), 'dist', 'services'),
|
|
58
|
-
join(process.cwd(), 'services'),
|
|
59
|
-
// Current working directory node_modules
|
|
60
|
-
join(process.cwd(), 'node_modules', '@justin_666', 'square-couplets-master-skills', 'dist', 'services'),
|
|
61
|
-
];
|
|
62
|
-
|
|
63
|
-
for (const path of possiblePaths) {
|
|
64
|
-
try {
|
|
65
|
-
if (statSync(path).isDirectory()) {
|
|
66
|
-
return path;
|
|
67
|
-
}
|
|
68
|
-
} catch (e) {
|
|
69
|
-
// Path doesn't exist, try next
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
return null;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// Load environment variables helper
|
|
76
|
-
async function loadEnvironmentVariables() {
|
|
24
|
+
async function main() {
|
|
77
25
|
try {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const cwdEnvPath = join(process.cwd(), '.env');
|
|
83
|
-
|
|
84
|
-
if (existsSync(envLocalPath)) {
|
|
85
|
-
dotenv.config({ path: envLocalPath });
|
|
86
|
-
} else if (existsSync(envPath)) {
|
|
87
|
-
dotenv.config({ path: envPath });
|
|
88
|
-
} else if (existsSync(cwdEnvLocalPath)) {
|
|
89
|
-
dotenv.config({ path: cwdEnvLocalPath });
|
|
90
|
-
} else if (existsSync(cwdEnvPath)) {
|
|
91
|
-
dotenv.config({ path: cwdEnvPath });
|
|
92
|
-
} else {
|
|
93
|
-
dotenv.config();
|
|
26
|
+
// Check for version flag
|
|
27
|
+
if (process.argv.includes('--version') || process.argv.includes('-v')) {
|
|
28
|
+
showVersion(projectRoot);
|
|
29
|
+
process.exit(0);
|
|
94
30
|
}
|
|
95
|
-
} catch (e) {
|
|
96
|
-
// dotenv not available, continue without it (will use environment variables)
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
31
|
|
|
100
|
-
async function main() {
|
|
101
|
-
try {
|
|
102
32
|
// Load environment variables first
|
|
103
|
-
await loadEnvironmentVariables();
|
|
33
|
+
await loadEnvironmentVariables(projectRoot);
|
|
104
34
|
|
|
105
35
|
// Parse command line arguments
|
|
106
|
-
const args = process.argv.slice(2);
|
|
36
|
+
const args = process.argv.slice(2).filter(arg => !arg.startsWith('-'));
|
|
107
37
|
const prompt = args[0];
|
|
108
38
|
const model = args[1] || 'gemini-2.5-flash-image';
|
|
109
39
|
const imageSize = args[2] || '1K';
|
|
@@ -113,17 +43,19 @@ async function main() {
|
|
|
113
43
|
if (!prompt) {
|
|
114
44
|
console.error('❌ Error: Prompt is required');
|
|
115
45
|
console.log('\nUsage:');
|
|
116
|
-
console.log('
|
|
46
|
+
console.log(' doufang-image <prompt> [model] [size] [reference-image] [output-path]');
|
|
117
47
|
console.log('\nParameters:');
|
|
118
48
|
console.log(' prompt - Image generation prompt (required)');
|
|
119
49
|
console.log(' model - Model to use: gemini-2.5-flash-image (default) or gemini-3-pro-image-preview');
|
|
120
50
|
console.log(' size - Image size: 1K (default), 2K, or 4K (Pro model only)');
|
|
121
51
|
console.log(' reference-image - Optional reference image path');
|
|
122
52
|
console.log(' output-path - Optional output file path (default: output/doufang-{timestamp}.png)');
|
|
53
|
+
console.log('\nOptions:');
|
|
54
|
+
console.log(' -v, --version Show version number');
|
|
123
55
|
console.log('\nExample:');
|
|
124
|
-
console.log('
|
|
125
|
-
console.log('
|
|
126
|
-
console.log('
|
|
56
|
+
console.log(' doufang-image "A diamond-shaped Doufang..."');
|
|
57
|
+
console.log(' doufang-image "..." gemini-3-pro-image-preview 2K');
|
|
58
|
+
console.log(' doufang-image "..." gemini-3-pro-image-preview 2K images/ref.png output/my-doufang.png');
|
|
127
59
|
process.exit(1);
|
|
128
60
|
}
|
|
129
61
|
|
|
@@ -141,91 +73,60 @@ async function main() {
|
|
|
141
73
|
}
|
|
142
74
|
|
|
143
75
|
// Get API key
|
|
144
|
-
const apiKey =
|
|
145
|
-
|
|
76
|
+
const apiKey = getApiKey();
|
|
146
77
|
if (!apiKey) {
|
|
147
78
|
console.error('❌ Error: API Key is missing');
|
|
148
79
|
console.log('💡 Set GEMINI_API_KEY in .env file or environment variable');
|
|
149
80
|
process.exit(1);
|
|
150
81
|
}
|
|
151
82
|
|
|
152
|
-
//
|
|
153
|
-
const servicesPath = findServicesPath();
|
|
83
|
+
// Find and import service module
|
|
84
|
+
const servicesPath = findServicesPath(projectRoot);
|
|
154
85
|
if (!servicesPath) {
|
|
155
86
|
console.error('❌ Error: Cannot find services directory');
|
|
156
87
|
console.log('💡 Make sure you are running from the project root or have installed the package');
|
|
88
|
+
console.log('💡 Set DEBUG_DOUFANG=1 to see detailed path checking');
|
|
157
89
|
process.exit(1);
|
|
158
90
|
}
|
|
159
91
|
|
|
160
|
-
|
|
161
|
-
let serviceModule;
|
|
162
|
-
const possibleServicePaths = [
|
|
163
|
-
// 1. Compiled JS in dist/ (npm package)
|
|
164
|
-
join(dirname(servicesPath), 'dist', 'services', 'geminiService.js'),
|
|
165
|
-
// 2. Compiled JS in services/ (if built locally)
|
|
166
|
-
join(servicesPath, 'geminiService.js'),
|
|
167
|
-
// 3. Source TS (development only)
|
|
168
|
-
join(servicesPath, 'geminiService.ts'),
|
|
169
|
-
];
|
|
170
|
-
|
|
171
|
-
let importError = null;
|
|
172
|
-
for (const servicePath of possibleServicePaths) {
|
|
173
|
-
try {
|
|
174
|
-
if (existsSync(servicePath)) {
|
|
175
|
-
serviceModule = await import(`file://${servicePath}`);
|
|
176
|
-
break;
|
|
177
|
-
}
|
|
178
|
-
} catch (e) {
|
|
179
|
-
importError = e;
|
|
180
|
-
continue;
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
if (!serviceModule) {
|
|
185
|
-
console.error('❌ Error: Cannot import service module');
|
|
186
|
-
console.error(' Tried paths:');
|
|
187
|
-
possibleServicePaths.forEach(p => console.error(` - ${p}`));
|
|
188
|
-
if (importError) {
|
|
189
|
-
console.error('\n Last error:', importError.message);
|
|
190
|
-
}
|
|
191
|
-
console.error('\n💡 This usually means the package was not properly built.');
|
|
192
|
-
console.error(' Please report this issue at: https://github.com/poirotw66/Square_Couplets_Master/issues');
|
|
193
|
-
process.exit(1);
|
|
194
|
-
}
|
|
92
|
+
const serviceModule = await importServiceModule(servicesPath);
|
|
195
93
|
const { generateDoufangImage } = serviceModule;
|
|
196
94
|
|
|
197
95
|
// Load reference image if provided
|
|
198
96
|
let referenceImageDataUrl = null;
|
|
199
97
|
if (referenceImagePath) {
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
console.
|
|
98
|
+
try {
|
|
99
|
+
referenceImageDataUrl = loadReferenceImage(referenceImagePath);
|
|
100
|
+
console.log(`🖼️ Using reference image: ${referenceImagePath}`);
|
|
101
|
+
} catch (error) {
|
|
102
|
+
console.error(`❌ Error: ${error.message}`);
|
|
203
103
|
process.exit(1);
|
|
204
104
|
}
|
|
205
|
-
|
|
206
|
-
const imageBuffer = readFileSync(fullPath);
|
|
207
|
-
const base64 = imageBuffer.toString('base64');
|
|
208
|
-
const ext = referenceImagePath.split('.').pop()?.toLowerCase();
|
|
209
|
-
const mimeType = ext === 'jpg' || ext === 'jpeg' ? 'image/jpeg' : 'image/png';
|
|
210
|
-
referenceImageDataUrl = `data:${mimeType};base64,${base64}`;
|
|
211
105
|
}
|
|
212
106
|
|
|
213
|
-
// Generate image
|
|
214
|
-
console.log(
|
|
107
|
+
// Generate image with progress spinner
|
|
108
|
+
console.log(`🎨 Generating image...`);
|
|
215
109
|
console.log(` Model: ${model}`);
|
|
216
110
|
console.log(` Size: ${imageSize}`);
|
|
217
|
-
|
|
218
|
-
console.log(` Reference: ${referenceImagePath}`);
|
|
219
|
-
}
|
|
220
|
-
console.log(' This may take a while, please wait...\n');
|
|
111
|
+
console.log('');
|
|
221
112
|
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
113
|
+
const spinner = createProgressSpinner('Generating image... (this may take 30-60 seconds)');
|
|
114
|
+
spinner.start();
|
|
115
|
+
|
|
116
|
+
let imageDataUrl;
|
|
117
|
+
try {
|
|
118
|
+
imageDataUrl = await generateDoufangImage(
|
|
119
|
+
prompt,
|
|
120
|
+
apiKey,
|
|
121
|
+
model,
|
|
122
|
+
imageSize,
|
|
123
|
+
referenceImageDataUrl
|
|
124
|
+
);
|
|
125
|
+
spinner.stop('✅ Image generated successfully!');
|
|
126
|
+
} catch (error) {
|
|
127
|
+
spinner.stop('');
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
229
130
|
|
|
230
131
|
// Extract base64 data
|
|
231
132
|
const base64Data = imageDataUrl.replace(/^data:image\/\w+;base64,/, '');
|
|
@@ -252,7 +153,6 @@ async function main() {
|
|
|
252
153
|
// Save image
|
|
253
154
|
writeFileSync(finalOutputPath, buffer);
|
|
254
155
|
|
|
255
|
-
console.log('✅ Image generated successfully!');
|
|
256
156
|
console.log(`📁 Saved to: ${finalOutputPath}`);
|
|
257
157
|
console.log(`📊 File size: ${(buffer.length / 1024 / 1024).toFixed(2)} MB`);
|
|
258
158
|
|
|
@@ -262,7 +162,18 @@ async function main() {
|
|
|
262
162
|
|
|
263
163
|
} catch (error) {
|
|
264
164
|
console.error('❌ Error:', error.message);
|
|
265
|
-
if (error.
|
|
165
|
+
if (error.details) {
|
|
166
|
+
console.error('\nDetails:');
|
|
167
|
+
if (error.details.triedPaths) {
|
|
168
|
+
console.error(' Tried paths:');
|
|
169
|
+
error.details.triedPaths.forEach(p => console.error(` - ${p}`));
|
|
170
|
+
}
|
|
171
|
+
if (error.details.lastError) {
|
|
172
|
+
console.error(` Last error: ${error.details.lastError}`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (process.env.DEBUG_DOUFANG && error.stack) {
|
|
176
|
+
console.error('\nStack trace:');
|
|
266
177
|
console.error(error.stack);
|
|
267
178
|
}
|
|
268
179
|
process.exit(1);
|
|
@@ -31,7 +31,7 @@ When user provides a keyword or wish phrase, generate a professional Doufang art
|
|
|
31
31
|
- **Decorative elements**: Symbolic elements that visually represent the keyword (e.g., horse, dragon, pine tree, crane, gold ingots, clouds, mountains, sun, plum blossoms)
|
|
32
32
|
- **Style**: Traditional Chinese ink painting mixed with realistic illustration, elegant, prestigious, festive but high-class
|
|
33
33
|
- **Composition**:
|
|
34
|
-
- Doufang fills 95% of the frame
|
|
34
|
+
- Doufang fills 90 - 95% of the frame
|
|
35
35
|
- Centered with minimal elegant margins (2-5% of frame width, just enough to prevent edge cropping)
|
|
36
36
|
- Fully visible inside the frame, not touching edges
|
|
37
37
|
- **Quality**: Ultra high detail, 8k, masterpiece, professional artwork, 1:1 aspect ratio
|
|
@@ -56,7 +56,7 @@ When user provides a keyword or wish phrase, generate a professional Doufang art
|
|
|
56
56
|
```json
|
|
57
57
|
{
|
|
58
58
|
"blessingPhrase": "招財進寶",
|
|
59
|
-
"imagePrompt": "A diamond-shaped Chinese New Year Doufang couplet on antique gold-flecked red Xuan paper. Central theme: bold, powerful, energetic traditional Chinese ink wash calligraphy of the characters '招財進寶'. Around the calligraphy: symbolic elements that visually represent wealth - gold ingots, coins, treasure chests, and prosperity symbols, painted in traditional Chinese ink painting style. Style: traditional Chinese ink painting mixed with realistic illustration, elegant, prestigious, festive but high-class, not cartoon. Material & texture: real Xuan paper texture, gold flecks, red rice paper, visible paper fibers, natural ink diffusion, subtle embossed gold foil details. Color theme: deep Chinese red, gold, black ink, warm highlights. Lighting: soft studio lighting, gentle glow on gold details, museum-quality artwork. Composition: The diamond-shaped Doufang fills 95% of the 1:1 frame, centered with minimal elegant margins (approximately 2-5% of frame width, just enough to prevent edge cropping). The entire artwork is fully visible inside the frame, not touching any edge, not cropped, not cut off. Clean background, symmetrical, perfectly framed, suitable for printing and hanging on wall. Quality: ultra high detail, 8k, masterpiece, professional artwork, 1:1 aspect ratio."
|
|
59
|
+
"imagePrompt": "A diamond-shaped Chinese New Year Doufang couplet on antique gold-flecked red Xuan paper. Central theme: bold, powerful, energetic traditional Chinese ink wash calligraphy of the characters '招財進寶'. Around the calligraphy: symbolic elements that visually represent wealth - gold ingots, coins, treasure chests, and prosperity symbols, painted in traditional Chinese ink painting style. Style: traditional Chinese ink painting mixed with realistic illustration, elegant, prestigious, festive but high-class, not cartoon. Material & texture: real Xuan paper texture, gold flecks, red rice paper, visible paper fibers, natural ink diffusion, subtle embossed gold foil details. Color theme: deep Chinese red, gold, black ink, warm highlights. Lighting: soft studio lighting, gentle glow on gold details, museum-quality artwork. Composition: The diamond-shaped Doufang fills 90 - 95% of the 1:1 frame, centered with minimal elegant margins (approximately 2-5% of frame width, just enough to prevent edge cropping). The entire artwork is fully visible inside the frame, not touching any edge, not cropped, not cut off. Clean background, symmetrical, perfectly framed, suitable for printing and hanging on wall. Quality: ultra high detail, 8k, masterpiece, professional artwork, 1:1 aspect ratio."
|
|
60
60
|
}
|
|
61
61
|
```
|
|
62
62
|
|
|
@@ -68,7 +68,7 @@ When user provides a keyword or wish phrase, generate a professional Doufang art
|
|
|
68
68
|
```json
|
|
69
69
|
{
|
|
70
70
|
"blessingPhrase": "延年益壽",
|
|
71
|
-
"imagePrompt": "A diamond-shaped Chinese New Year Doufang couplet on antique gold-flecked red Xuan paper. Central theme: bold, powerful, energetic traditional Chinese ink wash calligraphy of the characters '延年益壽'. Around the calligraphy: symbolic elements that visually represent health and longevity - pine trees, cranes, peaches, and bamboo, painted in traditional Chinese ink painting style. Style: traditional Chinese ink painting mixed with realistic illustration, elegant, prestigious, festive but high-class, not cartoon. Material & texture: real Xuan paper texture, gold flecks, red rice paper, visible paper fibers, natural ink diffusion, subtle embossed gold foil details. Color theme: deep Chinese red, gold, black ink, warm highlights. Lighting: soft studio lighting, gentle glow on gold details, museum-quality artwork. Composition: The diamond-shaped Doufang fills 95% of the 1:1 frame, centered with minimal elegant margins (approximately 2-5% of frame width, just enough to prevent edge cropping). The entire artwork is fully visible inside the frame, not touching any edge, not cropped, not cut off. Clean background, symmetrical, perfectly framed, suitable for printing and hanging on wall. Quality: ultra high detail, 8k, masterpiece, professional artwork, 1:1 aspect ratio."
|
|
71
|
+
"imagePrompt": "A diamond-shaped Chinese New Year Doufang couplet on antique gold-flecked red Xuan paper. Central theme: bold, powerful, energetic traditional Chinese ink wash calligraphy of the characters '延年益壽'. Around the calligraphy: symbolic elements that visually represent health and longevity - pine trees, cranes, peaches, and bamboo, painted in traditional Chinese ink painting style. Style: traditional Chinese ink painting mixed with realistic illustration, elegant, prestigious, festive but high-class, not cartoon. Material & texture: real Xuan paper texture, gold flecks, red rice paper, visible paper fibers, natural ink diffusion, subtle embossed gold foil details. Color theme: deep Chinese red, gold, black ink, warm highlights. Lighting: soft studio lighting, gentle glow on gold details, museum-quality artwork. Composition: The diamond-shaped Doufang fills 90 - 95% of the 1:1 frame, centered with minimal elegant margins (approximately 2-5% of frame width, just enough to prevent edge cropping). The entire artwork is fully visible inside the frame, not touching any edge, not cropped, not cut off. Clean background, symmetrical, perfectly framed, suitable for printing and hanging on wall. Quality: ultra high detail, 8k, masterpiece, professional artwork, 1:1 aspect ratio."
|
|
72
72
|
}
|
|
73
73
|
```
|
|
74
74
|
|
|
@@ -80,13 +80,13 @@ When user provides a keyword or wish phrase, generate a professional Doufang art
|
|
|
80
80
|
```json
|
|
81
81
|
{
|
|
82
82
|
"blessingPhrase": "萬馬奔騰",
|
|
83
|
-
"imagePrompt": "A diamond-shaped Chinese New Year Doufang couplet on antique gold-flecked red Xuan paper. Central theme: bold, powerful, energetic traditional Chinese ink wash calligraphy of the characters '萬馬奔騰'. Around the calligraphy: symbolic elements that visually represent energy and vitality - galloping horses, flowing clouds, dynamic movement, painted in traditional Chinese ink painting style. Style: traditional Chinese ink painting mixed with realistic illustration, elegant, prestigious, festive but high-class, not cartoon. Material & texture: real Xuan paper texture, gold flecks, red rice paper, visible paper fibers, natural ink diffusion, subtle embossed gold foil details. Color theme: deep Chinese red, gold, black ink, warm highlights. Lighting: soft studio lighting, gentle glow on gold details, museum-quality artwork. Composition: The diamond-shaped Doufang fills 95% of the 1:1 frame, centered with minimal elegant margins (approximately 2-5% of frame width, just enough to prevent edge cropping). The entire artwork is fully visible inside the frame, not touching any edge, not cropped, not cut off. Clean background, symmetrical, perfectly framed, suitable for printing and hanging on wall. Quality: ultra high detail, 8k, masterpiece, professional artwork, 1:1 aspect ratio."
|
|
83
|
+
"imagePrompt": "A diamond-shaped Chinese New Year Doufang couplet on antique gold-flecked red Xuan paper. Central theme: bold, powerful, energetic traditional Chinese ink wash calligraphy of the characters '萬馬奔騰'. Around the calligraphy: symbolic elements that visually represent energy and vitality - galloping horses, flowing clouds, dynamic movement, painted in traditional Chinese ink painting style. Style: traditional Chinese ink painting mixed with realistic illustration, elegant, prestigious, festive but high-class, not cartoon. Material & texture: real Xuan paper texture, gold flecks, red rice paper, visible paper fibers, natural ink diffusion, subtle embossed gold foil details. Color theme: deep Chinese red, gold, black ink, warm highlights. Lighting: soft studio lighting, gentle glow on gold details, museum-quality artwork. Composition: The diamond-shaped Doufang fills 90 - 95% of the 1:1 frame, centered with minimal elegant margins (approximately 2-5% of frame width, just enough to prevent edge cropping). The entire artwork is fully visible inside the frame, not touching any edge, not cropped, not cut off. Clean background, symmetrical, perfectly framed, suitable for printing and hanging on wall. Quality: ultra high detail, 8k, masterpiece, professional artwork, 1:1 aspect ratio."
|
|
84
84
|
}
|
|
85
85
|
```
|
|
86
86
|
|
|
87
87
|
## Key Requirements
|
|
88
88
|
|
|
89
|
-
- The Doufang should fill **95% of the frame** (not less)
|
|
89
|
+
- The Doufang should fill **90 - 95% of the frame** (not less)
|
|
90
90
|
- Minimal margins: **2-5% of frame width** (just enough to prevent edge cropping)
|
|
91
91
|
- **No excessive white space** or wide margins
|
|
92
92
|
- Traditional Chinese artistic style (ink wash, calligraphy)
|
|
@@ -5,218 +5,82 @@
|
|
|
5
5
|
* Can be called directly by agents or users
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
8
|
+
import { resolve } from 'path';
|
|
9
|
+
import {
|
|
10
|
+
getProjectRoot,
|
|
11
|
+
loadEnvironmentVariables,
|
|
12
|
+
findServicesPath,
|
|
13
|
+
getApiKey,
|
|
14
|
+
showVersion,
|
|
15
|
+
loadReferenceImage,
|
|
16
|
+
importServiceModule
|
|
17
|
+
} from '../shared/utils.js';
|
|
18
|
+
|
|
19
|
+
// Initialize
|
|
20
|
+
const projectRoot = getProjectRoot(import.meta.url);
|
|
13
21
|
|
|
14
|
-
|
|
15
|
-
async function loadEnvironmentVariables() {
|
|
16
|
-
try {
|
|
17
|
-
const dotenv = await import('dotenv');
|
|
18
|
-
const envLocalPath = join(projectRoot, '.env.local');
|
|
19
|
-
const envPath = join(projectRoot, '.env');
|
|
20
|
-
const cwdEnvLocalPath = join(process.cwd(), '.env.local');
|
|
21
|
-
const cwdEnvPath = join(process.cwd(), '.env');
|
|
22
|
-
|
|
23
|
-
if (existsSync(envLocalPath)) {
|
|
24
|
-
dotenv.config({ path: envLocalPath });
|
|
25
|
-
} else if (existsSync(envPath)) {
|
|
26
|
-
dotenv.config({ path: envPath });
|
|
27
|
-
} else if (existsSync(cwdEnvLocalPath)) {
|
|
28
|
-
dotenv.config({ path: cwdEnvLocalPath });
|
|
29
|
-
} else if (existsSync(cwdEnvPath)) {
|
|
30
|
-
dotenv.config({ path: cwdEnvPath });
|
|
31
|
-
} else {
|
|
32
|
-
dotenv.config();
|
|
33
|
-
}
|
|
34
|
-
} catch (e) {
|
|
35
|
-
// dotenv not available, continue without it (will use environment variables)
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// Resolve project root and service path
|
|
40
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
41
|
-
const __dirname = dirname(__filename);
|
|
42
|
-
const skillDir = resolve(__dirname);
|
|
43
|
-
const projectRoot = resolve(skillDir, '../..');
|
|
44
|
-
|
|
45
|
-
// Try to find dist directory (compiled JS) or services directory (source TS)
|
|
46
|
-
function findServicesPath() {
|
|
47
|
-
// Get npm global prefix to find globally installed packages
|
|
48
|
-
let globalPrefix = null;
|
|
49
|
-
try {
|
|
50
|
-
globalPrefix = execSync('npm config get prefix', { encoding: 'utf-8' }).trim();
|
|
51
|
-
} catch (e) {
|
|
52
|
-
// Ignore error, try other methods
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// Try to resolve package location using createRequire (works in ES modules)
|
|
56
|
-
let packageRoot = null;
|
|
22
|
+
async function main() {
|
|
57
23
|
try {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
// Package not found via require.resolve, will try other paths
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// Build list of possible paths
|
|
66
|
-
// IMPORTANT: We're looking for dist/services (compiled) not services/ (source)
|
|
67
|
-
const possiblePaths = [
|
|
68
|
-
// Global npm package - dist directory (compiled JS)
|
|
69
|
-
...(globalPrefix ? [
|
|
70
|
-
join(globalPrefix, 'lib', 'node_modules', '@justin_666', 'square-couplets-master-skills', 'dist', 'services'),
|
|
71
|
-
join(globalPrefix, 'node_modules', '@justin_666', 'square-couplets-master-skills', 'dist', 'services'),
|
|
72
|
-
] : []),
|
|
73
|
-
// From resolved package root - dist directory
|
|
74
|
-
...(packageRoot ? [
|
|
75
|
-
join(packageRoot, 'dist', 'services'),
|
|
76
|
-
join(packageRoot, 'services'), // fallback to source
|
|
77
|
-
] : []),
|
|
78
|
-
// Local project root - dist directory
|
|
79
|
-
join(projectRoot, 'dist', 'services'),
|
|
80
|
-
join(projectRoot, 'services'), // fallback to source
|
|
81
|
-
// Local node_modules - dist directory
|
|
82
|
-
join(projectRoot, 'node_modules', '@justin_666', 'square-couplets-master-skills', 'dist', 'services'),
|
|
83
|
-
// Current working directory - dist directory
|
|
84
|
-
join(process.cwd(), 'dist', 'services'),
|
|
85
|
-
join(process.cwd(), 'services'),
|
|
86
|
-
// Current working directory node_modules
|
|
87
|
-
join(process.cwd(), 'node_modules', '@justin_666', 'square-couplets-master-skills', 'dist', 'services'),
|
|
88
|
-
];
|
|
89
|
-
|
|
90
|
-
// Debug: log all paths being checked (enable with DEBUG_DOUFANG=1)
|
|
91
|
-
if (process.env.DEBUG_DOUFANG) {
|
|
92
|
-
console.log('🔍 Checking paths for services directory:');
|
|
93
|
-
console.log(` packageRoot: ${packageRoot || 'not found'}`);
|
|
94
|
-
console.log(` globalPrefix: ${globalPrefix || 'not found'}`);
|
|
95
|
-
console.log(` projectRoot: ${projectRoot}`);
|
|
96
|
-
console.log(` cwd: ${process.cwd()}`);
|
|
97
|
-
console.log('\n Trying paths:');
|
|
98
|
-
for (const path of possiblePaths) {
|
|
99
|
-
const exists = existsSync(path);
|
|
100
|
-
console.log(` ${exists ? '✅' : '❌'} ${path}`);
|
|
24
|
+
// Check for version flag
|
|
25
|
+
if (process.argv.includes('--version') || process.argv.includes('-v')) {
|
|
26
|
+
showVersion(projectRoot);
|
|
27
|
+
process.exit(0);
|
|
101
28
|
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
for (const path of possiblePaths) {
|
|
105
|
-
try {
|
|
106
|
-
if (statSync(path).isDirectory()) {
|
|
107
|
-
if (process.env.DEBUG_DOUFANG) {
|
|
108
|
-
console.log(`\n✅ Found services at: ${path}`);
|
|
109
|
-
}
|
|
110
|
-
return path;
|
|
111
|
-
}
|
|
112
|
-
} catch (e) {
|
|
113
|
-
// Path doesn't exist, try next
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// If not found, provide helpful error message
|
|
118
|
-
if (process.env.DEBUG_DOUFANG) {
|
|
119
|
-
console.log('\n❌ Services directory not found in any of the checked paths');
|
|
120
|
-
}
|
|
121
|
-
return null;
|
|
122
|
-
}
|
|
123
29
|
|
|
124
|
-
|
|
125
|
-
async function main() {
|
|
126
|
-
try {
|
|
127
30
|
// Load environment variables first
|
|
128
|
-
await loadEnvironmentVariables();
|
|
31
|
+
await loadEnvironmentVariables(projectRoot);
|
|
129
32
|
|
|
130
33
|
// Parse command line arguments
|
|
131
|
-
const args = process.argv.slice(2);
|
|
34
|
+
const args = process.argv.slice(2).filter(arg => !arg.startsWith('-'));
|
|
132
35
|
const keyword = args[0];
|
|
133
36
|
const referenceImagePath = args[1]; // Optional reference image path
|
|
134
37
|
|
|
135
38
|
if (!keyword) {
|
|
136
39
|
console.error('❌ Error: Keyword is required');
|
|
137
40
|
console.log('\nUsage:');
|
|
138
|
-
console.log('
|
|
41
|
+
console.log(' doufang-prompt <keyword> [reference-image-path]');
|
|
42
|
+
console.log('\nOptions:');
|
|
43
|
+
console.log(' -v, --version Show version number');
|
|
139
44
|
console.log('\nExample:');
|
|
140
|
-
console.log('
|
|
141
|
-
console.log('
|
|
45
|
+
console.log(' doufang-prompt "財富"');
|
|
46
|
+
console.log(' doufang-prompt "健康" images/reference.png');
|
|
142
47
|
process.exit(1);
|
|
143
48
|
}
|
|
144
49
|
|
|
145
50
|
// Get API key
|
|
146
|
-
const apiKey =
|
|
147
|
-
|
|
51
|
+
const apiKey = getApiKey();
|
|
148
52
|
if (!apiKey) {
|
|
149
53
|
console.error('❌ Error: API Key is missing');
|
|
150
54
|
console.log('💡 Set GEMINI_API_KEY in .env file or environment variable');
|
|
151
55
|
process.exit(1);
|
|
152
56
|
}
|
|
153
57
|
|
|
154
|
-
//
|
|
155
|
-
const servicesPath = findServicesPath();
|
|
58
|
+
// Find and import service module
|
|
59
|
+
const servicesPath = findServicesPath(projectRoot);
|
|
156
60
|
if (!servicesPath) {
|
|
157
61
|
console.error('❌ Error: Cannot find services directory');
|
|
158
62
|
console.log('💡 Make sure you are running from the project root or have installed the package');
|
|
63
|
+
console.log('💡 Set DEBUG_DOUFANG=1 to see detailed path checking');
|
|
159
64
|
process.exit(1);
|
|
160
65
|
}
|
|
161
66
|
|
|
162
|
-
|
|
163
|
-
let serviceModule;
|
|
164
|
-
const possibleServicePaths = [
|
|
165
|
-
// 1. Compiled JS in dist/ (npm package)
|
|
166
|
-
join(dirname(servicesPath), 'dist', 'services', 'geminiService.js'),
|
|
167
|
-
// 2. Compiled JS in services/ (if built locally)
|
|
168
|
-
join(servicesPath, 'geminiService.js'),
|
|
169
|
-
// 3. Source TS (development only)
|
|
170
|
-
join(servicesPath, 'geminiService.ts'),
|
|
171
|
-
];
|
|
172
|
-
|
|
173
|
-
let importError = null;
|
|
174
|
-
for (const servicePath of possibleServicePaths) {
|
|
175
|
-
try {
|
|
176
|
-
if (existsSync(servicePath)) {
|
|
177
|
-
serviceModule = await import(`file://${servicePath}`);
|
|
178
|
-
break;
|
|
179
|
-
}
|
|
180
|
-
} catch (e) {
|
|
181
|
-
importError = e;
|
|
182
|
-
continue;
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
if (!serviceModule) {
|
|
187
|
-
console.error('❌ Error: Cannot import service module');
|
|
188
|
-
console.error(' Tried paths:');
|
|
189
|
-
possibleServicePaths.forEach(p => console.error(` - ${p}`));
|
|
190
|
-
if (importError) {
|
|
191
|
-
console.error('\n Last error:', importError.message);
|
|
192
|
-
}
|
|
193
|
-
console.error('\n💡 This usually means the package was not properly built.');
|
|
194
|
-
console.error(' Please report this issue at: https://github.com/poirotw66/Square_Couplets_Master/issues');
|
|
195
|
-
process.exit(1);
|
|
196
|
-
}
|
|
67
|
+
const serviceModule = await importServiceModule(servicesPath);
|
|
197
68
|
const { generateDoufangPrompt } = serviceModule;
|
|
198
69
|
|
|
199
70
|
// Load reference image if provided
|
|
200
71
|
let referenceImageDataUrl = null;
|
|
201
72
|
if (referenceImagePath) {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
console.
|
|
73
|
+
try {
|
|
74
|
+
referenceImageDataUrl = loadReferenceImage(referenceImagePath);
|
|
75
|
+
console.log(`🖼️ Using reference image: ${referenceImagePath}`);
|
|
76
|
+
} catch (error) {
|
|
77
|
+
console.error(`❌ Error: ${error.message}`);
|
|
205
78
|
process.exit(1);
|
|
206
79
|
}
|
|
207
|
-
|
|
208
|
-
const imageBuffer = readFileSync(fullPath);
|
|
209
|
-
const base64 = imageBuffer.toString('base64');
|
|
210
|
-
const ext = referenceImagePath.split('.').pop()?.toLowerCase();
|
|
211
|
-
const mimeType = ext === 'jpg' || ext === 'jpeg' ? 'image/jpeg' : 'image/png';
|
|
212
|
-
referenceImageDataUrl = `data:${mimeType};base64,${base64}`;
|
|
213
80
|
}
|
|
214
81
|
|
|
215
82
|
// Generate prompt
|
|
216
83
|
console.log(`📝 Generating prompt for keyword: "${keyword}"`);
|
|
217
|
-
if (referenceImagePath) {
|
|
218
|
-
console.log(`🖼️ Using reference image: ${referenceImagePath}`);
|
|
219
|
-
}
|
|
220
84
|
|
|
221
85
|
const result = await generateDoufangPrompt(keyword, apiKey, referenceImageDataUrl);
|
|
222
86
|
|
|
@@ -225,7 +89,18 @@ async function main() {
|
|
|
225
89
|
|
|
226
90
|
} catch (error) {
|
|
227
91
|
console.error('❌ Error:', error.message);
|
|
228
|
-
if (error.
|
|
92
|
+
if (error.details) {
|
|
93
|
+
console.error('\nDetails:');
|
|
94
|
+
if (error.details.triedPaths) {
|
|
95
|
+
console.error(' Tried paths:');
|
|
96
|
+
error.details.triedPaths.forEach(p => console.error(` - ${p}`));
|
|
97
|
+
}
|
|
98
|
+
if (error.details.lastError) {
|
|
99
|
+
console.error(` Last error: ${error.details.lastError}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (process.env.DEBUG_DOUFANG && error.stack) {
|
|
103
|
+
console.error('\nStack trace:');
|
|
229
104
|
console.error(error.stack);
|
|
230
105
|
}
|
|
231
106
|
process.exit(1);
|
|
@@ -26,11 +26,11 @@ When user reports that generated images have excessive white margins, poor compo
|
|
|
26
26
|
|
|
27
27
|
**Replace with:**
|
|
28
28
|
- ✅ "centered with minimal elegant margins (approximately 2-5% of frame width, just enough to prevent edge cropping)"
|
|
29
|
-
- ✅ "minimal margins - the Doufang should fill most of the frame (95% of image area)"
|
|
30
|
-
- ✅ "The Doufang should occupy 95% of the image area, maximizing visual impact"
|
|
29
|
+
- ✅ "minimal margins - the Doufang should fill most of the frame (90 - 95% of image area)"
|
|
30
|
+
- ✅ "The Doufang should occupy 90 - 95% of the image area, maximizing visual impact"
|
|
31
31
|
|
|
32
32
|
3. **Add explicit composition instructions:**
|
|
33
|
-
- Add: "The Doufang should fill 95% of the frame"
|
|
33
|
+
- Add: "The Doufang should fill 90 - 95% of the frame"
|
|
34
34
|
- Add: "Minimal margins (2-5% of frame width)"
|
|
35
35
|
- Add: "Maximize visual impact by making the Doufang occupy most of the frame"
|
|
36
36
|
- Add: "Avoid excessive white space or wide margins"
|
|
@@ -61,7 +61,7 @@ When user reports that generated images have excessive white margins, poor compo
|
|
|
61
61
|
|
|
62
62
|
**After:**
|
|
63
63
|
```
|
|
64
|
-
"...The Doufang should occupy 95% of the image area, maximizing visual impact..."
|
|
64
|
+
"...The Doufang should occupy 90 - 95% of the image area, maximizing visual impact..."
|
|
65
65
|
```
|
|
66
66
|
|
|
67
67
|
### Rule 3: Composition Emphasis
|
|
@@ -72,7 +72,7 @@ When user reports that generated images have excessive white margins, poor compo
|
|
|
72
72
|
|
|
73
73
|
**After:**
|
|
74
74
|
```
|
|
75
|
-
"...minimal margins - the Doufang should fill most of the frame (95% of image area)..."
|
|
75
|
+
"...minimal margins - the Doufang should fill most of the frame (90 - 95% of image area)..."
|
|
76
76
|
```
|
|
77
77
|
|
|
78
78
|
## Examples
|
|
@@ -86,7 +86,7 @@ A diamond-shaped Chinese New Year Doufang centered in a 1:1 frame with wide whit
|
|
|
86
86
|
|
|
87
87
|
**Optimized Prompt:**
|
|
88
88
|
```
|
|
89
|
-
A diamond-shaped Chinese New Year Doufang fills the majority of the 1:1 frame, centered with minimal elegant margins (approximately 2-5% of frame width, just enough to prevent edge cropping). The Doufang should occupy 95% of the image area, maximizing visual impact. The artwork is a masterpiece of traditional ink-wash fusion...
|
|
89
|
+
A diamond-shaped Chinese New Year Doufang fills the majority of the 1:1 frame, centered with minimal elegant margins (approximately 2-5% of frame width, just enough to prevent edge cropping). The Doufang should occupy 90 - 95% of the image area, maximizing visual impact. The artwork is a masterpiece of traditional ink-wash fusion...
|
|
90
90
|
```
|
|
91
91
|
|
|
92
92
|
### Example 2: Improving Composition Instructions
|
|
@@ -98,7 +98,7 @@ Composition: The diamond-shaped Doufang is fully visible and centered, with gene
|
|
|
98
98
|
|
|
99
99
|
**Optimized Prompt:**
|
|
100
100
|
```
|
|
101
|
-
Composition: The diamond-shaped Doufang fills 95% of the 1:1 frame, centered with minimal elegant margins (approximately 2-5% of frame width, just enough to prevent edge cropping). The entire artwork is fully visible inside the frame, not touching any edge, not cropped, not cut off. The Doufang should occupy most of the image area, maximizing visual impact. Clean background, symmetrical, perfectly framed, suitable for printing and hanging on wall.
|
|
101
|
+
Composition: The diamond-shaped Doufang fills 90 - 95% of the 1:1 frame, centered with minimal elegant margins (approximately 2-5% of frame width, just enough to prevent edge cropping). The entire artwork is fully visible inside the frame, not touching any edge, not cropped, not cut off. The Doufang should occupy most of the image area, maximizing visual impact. Clean background, symmetrical, perfectly framed, suitable for printing and hanging on wall.
|
|
102
102
|
```
|
|
103
103
|
|
|
104
104
|
### Example 3: Complete Prompt Optimization
|
|
@@ -115,7 +115,7 @@ Framing requirements:
|
|
|
115
115
|
```
|
|
116
116
|
Framing requirements:
|
|
117
117
|
- The entire diamond-shaped Doufang must be fully visible inside the image.
|
|
118
|
-
- Minimal margins - the Doufang should fill most of the frame (95% of image area).
|
|
118
|
+
- Minimal margins - the Doufang should fill most of the frame (90 - 95% of image area).
|
|
119
119
|
- No cropping, no touching edges, no cut-off.
|
|
120
120
|
- Maximize visual impact by making the Doufang occupy most of the frame.
|
|
121
121
|
```
|
|
@@ -123,7 +123,7 @@ Framing requirements:
|
|
|
123
123
|
## Key Optimization Principles
|
|
124
124
|
|
|
125
125
|
1. **Never use "wide" or "generous" margins** - Always specify minimal margins (2-5%)
|
|
126
|
-
2. **Always specify frame fill percentage** - 95% of image area
|
|
126
|
+
2. **Always specify frame fill percentage** - 90 - 95% of image area
|
|
127
127
|
3. **Emphasize visual impact** - Over safety margins
|
|
128
128
|
4. **Be specific about margin size** - "2-5% of frame width"
|
|
129
129
|
5. **Clarify purpose** - "just enough to prevent edge cropping"
|
|
@@ -3,97 +3,178 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* Executable script for optimize-doufang-prompt skill
|
|
5
5
|
* Optimizes prompts to reduce white margins and improve composition
|
|
6
|
+
* Supports both simple rule-based and AI-powered optimization
|
|
6
7
|
*/
|
|
7
8
|
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
import {
|
|
10
|
+
getProjectRoot,
|
|
11
|
+
loadEnvironmentVariables,
|
|
12
|
+
getApiKey,
|
|
13
|
+
showVersion,
|
|
14
|
+
createProgressSpinner
|
|
15
|
+
} from '../shared/utils.js';
|
|
12
16
|
|
|
13
|
-
//
|
|
14
|
-
const
|
|
15
|
-
const __dirname = dirname(__filename);
|
|
16
|
-
const skillDir = resolve(__dirname);
|
|
17
|
-
const projectRoot = resolve(skillDir, '../..');
|
|
17
|
+
// Initialize
|
|
18
|
+
const projectRoot = getProjectRoot(import.meta.url);
|
|
18
19
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
20
|
+
/**
|
|
21
|
+
* Optimize prompt using AI (Gemini)
|
|
22
|
+
* Note: AI optimization is experimental and may fallback to rule-based
|
|
23
|
+
*/
|
|
24
|
+
async function optimizePromptWithAI(originalPrompt, apiKey) {
|
|
25
|
+
// Note: AI optimization requires proper Google Gen AI SDK integration
|
|
26
|
+
// For now, this is a placeholder that falls back to rule-based optimization
|
|
27
|
+
// Future implementation will use: GoogleGenAI API for smarter optimization
|
|
28
|
+
|
|
29
|
+
console.log('⚠️ AI optimization is currently experimental');
|
|
30
|
+
console.log(' Falling back to rule-based optimization');
|
|
31
|
+
return optimizePromptSimple(originalPrompt);
|
|
29
32
|
}
|
|
30
33
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
34
|
+
/**
|
|
35
|
+
* Optimize prompt using simple rules (fallback)
|
|
36
|
+
*/
|
|
37
|
+
function optimizePromptSimple(originalPrompt) {
|
|
35
38
|
let optimized = originalPrompt;
|
|
36
39
|
|
|
37
40
|
// Replace wide margin descriptions with minimal margin
|
|
38
41
|
optimized = optimized.replace(/wide\s+white\s+margins/gi, 'minimal elegant margins (2-5% of frame width)');
|
|
39
42
|
optimized = optimized.replace(/wide\s+margins/gi, 'minimal margins (2-5%)');
|
|
40
43
|
optimized = optimized.replace(/excessive\s+white\s+space/gi, 'minimal white space');
|
|
44
|
+
optimized = optimized.replace(/large\s+margins/gi, 'minimal margins (2-5%)');
|
|
41
45
|
|
|
42
|
-
// Ensure Doufang fills
|
|
43
|
-
if (!optimized.includes('85-95%')) {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
46
|
+
// Ensure Doufang fills 90-95% of frame
|
|
47
|
+
if (!optimized.includes('90-95%') && !optimized.includes('85-95%') && !optimized.includes('fills 90%')) {
|
|
48
|
+
// Try to find Composition section and enhance it
|
|
49
|
+
const hasComposition = /Composition:/i.test(optimized);
|
|
50
|
+
if (hasComposition) {
|
|
51
|
+
optimized = optimized.replace(
|
|
52
|
+
/(Composition:.*?)(\.|$)/i,
|
|
53
|
+
(match, p1, p2) => {
|
|
54
|
+
if (!p1.includes('90-95%') && !p1.includes('85-95%')) {
|
|
55
|
+
return p1 + ' The diamond-shaped Doufang fills 90-95% of the 1:1 frame, centered with minimal margins (2-5% of frame width).' + p2;
|
|
56
|
+
}
|
|
57
|
+
return match;
|
|
49
58
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
59
|
+
);
|
|
60
|
+
} else {
|
|
61
|
+
// Add composition requirement at the end
|
|
62
|
+
optimized += '\n\nComposition: The diamond-shaped Doufang fills 90-95% of the 1:1 frame, centered with minimal margins (2-5% of frame width). The entire artwork is fully visible inside the frame, not touching any edge, not cropped, not cut off.';
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Ensure "not cropped" and "not touching edges" are mentioned
|
|
67
|
+
if (!optimized.includes('not cropped') && !optimized.includes('not touch')) {
|
|
68
|
+
optimized += ' The artwork should not be cropped or touch the frame edges.';
|
|
53
69
|
}
|
|
54
70
|
|
|
55
71
|
// Add explicit margin requirements if not present
|
|
56
|
-
if (!optimized.includes('2-5%')) {
|
|
57
|
-
optimized += '\n\nIMPORTANT: The diamond-shaped Doufang must fill
|
|
72
|
+
if (!optimized.includes('2-5%') && !optimized.includes('minimal margin')) {
|
|
73
|
+
optimized += '\n\nIMPORTANT: The diamond-shaped Doufang must fill 90-95% of the frame with minimal margins (2-5% of frame width). Avoid excessive white space or wide margins.';
|
|
58
74
|
}
|
|
59
75
|
|
|
60
|
-
return optimized;
|
|
76
|
+
return optimized.trim();
|
|
61
77
|
}
|
|
62
78
|
|
|
63
79
|
async function main() {
|
|
64
80
|
try {
|
|
81
|
+
// Check for version flag
|
|
82
|
+
if (process.argv.includes('--version') || process.argv.includes('-v')) {
|
|
83
|
+
showVersion(projectRoot);
|
|
84
|
+
process.exit(0);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Load environment variables first
|
|
88
|
+
await loadEnvironmentVariables(projectRoot);
|
|
89
|
+
|
|
65
90
|
// Parse command line arguments
|
|
66
91
|
const args = process.argv.slice(2);
|
|
67
|
-
const
|
|
92
|
+
const useAI = args.includes('--ai');
|
|
93
|
+
const prompt = args.filter(arg => !arg.startsWith('--'))[0];
|
|
68
94
|
|
|
69
95
|
if (!prompt) {
|
|
70
96
|
console.error('❌ Error: Prompt is required');
|
|
71
97
|
console.log('\nUsage:');
|
|
72
|
-
console.log('
|
|
73
|
-
console.log('\
|
|
74
|
-
console.log('
|
|
98
|
+
console.log(' doufang-optimize <prompt> [--ai]');
|
|
99
|
+
console.log('\nOptions:');
|
|
100
|
+
console.log(' --ai Use AI to optimize prompt (requires API key)');
|
|
101
|
+
console.log(' -v, --version Show version number');
|
|
75
102
|
console.log('\nExample:');
|
|
76
|
-
console.log('
|
|
103
|
+
console.log(' doufang-optimize "A diamond-shaped Doufang with wide margins..."');
|
|
104
|
+
console.log(' doufang-optimize "..." --ai');
|
|
105
|
+
console.log('\nNote: Without --ai flag, uses fast rule-based optimization.');
|
|
106
|
+
console.log(' With --ai flag, uses Gemini AI for smarter optimization.');
|
|
77
107
|
process.exit(1);
|
|
78
108
|
}
|
|
79
109
|
|
|
80
|
-
// Optimize prompt
|
|
81
110
|
console.log('✨ Optimizing prompt...\n');
|
|
82
|
-
|
|
111
|
+
|
|
112
|
+
let optimized;
|
|
113
|
+
let method = 'rule-based';
|
|
114
|
+
|
|
115
|
+
if (useAI) {
|
|
116
|
+
const apiKey = getApiKey();
|
|
117
|
+
if (!apiKey) {
|
|
118
|
+
console.warn('⚠️ No API key found, falling back to rule-based optimization');
|
|
119
|
+
console.warn('💡 Set GEMINI_API_KEY to use AI-powered optimization\n');
|
|
120
|
+
optimized = optimizePromptSimple(prompt);
|
|
121
|
+
} else {
|
|
122
|
+
console.log('🤖 Using AI to optimize (Gemini 2.0 Flash)...');
|
|
123
|
+
const spinner = createProgressSpinner('Optimizing with AI...');
|
|
124
|
+
spinner.start();
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
optimized = await optimizePromptWithAI(prompt, apiKey);
|
|
128
|
+
spinner.stop('✅ AI optimization complete!');
|
|
129
|
+
method = 'AI-powered (Gemini 2.0 Flash)';
|
|
130
|
+
} catch (error) {
|
|
131
|
+
spinner.stop('');
|
|
132
|
+
console.warn(`⚠️ AI optimization failed: ${error.message}`);
|
|
133
|
+
console.warn(' Falling back to rule-based optimization\n');
|
|
134
|
+
optimized = optimizePromptSimple(prompt);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
} else {
|
|
138
|
+
console.log('⚡ Using fast rule-based optimization...');
|
|
139
|
+
optimized = optimizePromptSimple(prompt);
|
|
140
|
+
}
|
|
83
141
|
|
|
84
142
|
// Output optimized prompt
|
|
85
|
-
console.log('✅ Optimized prompt:');
|
|
143
|
+
console.log('\n✅ Optimized prompt:');
|
|
86
144
|
console.log('─'.repeat(60));
|
|
87
145
|
console.log(optimized);
|
|
88
146
|
console.log('─'.repeat(60));
|
|
89
147
|
|
|
148
|
+
// Calculate improvement (simple heuristic)
|
|
149
|
+
const improvements = [];
|
|
150
|
+
if (!prompt.includes('90-95%') && optimized.includes('90-95%')) {
|
|
151
|
+
improvements.push('Added frame fill percentage');
|
|
152
|
+
}
|
|
153
|
+
if (!prompt.includes('minimal margin') && optimized.includes('minimal margin')) {
|
|
154
|
+
improvements.push('Specified minimal margins');
|
|
155
|
+
}
|
|
156
|
+
if (prompt.includes('wide margin') && !optimized.includes('wide margin')) {
|
|
157
|
+
improvements.push('Removed wide margin references');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (improvements.length > 0) {
|
|
161
|
+
console.log('\n📊 Improvements:');
|
|
162
|
+
improvements.forEach(imp => console.log(` ✓ ${imp}`));
|
|
163
|
+
}
|
|
164
|
+
|
|
90
165
|
// Also output as JSON for programmatic use
|
|
91
166
|
console.log('\n📋 JSON output:');
|
|
92
|
-
console.log(JSON.stringify({
|
|
167
|
+
console.log(JSON.stringify({
|
|
168
|
+
originalPrompt: prompt,
|
|
169
|
+
optimizedPrompt: optimized,
|
|
170
|
+
method: method,
|
|
171
|
+
improvements: improvements
|
|
172
|
+
}, null, 2));
|
|
93
173
|
|
|
94
174
|
} catch (error) {
|
|
95
175
|
console.error('❌ Error:', error.message);
|
|
96
|
-
if (error.stack) {
|
|
176
|
+
if (process.env.DEBUG_DOUFANG && error.stack) {
|
|
177
|
+
console.error('\nStack trace:');
|
|
97
178
|
console.error(error.stack);
|
|
98
179
|
}
|
|
99
180
|
process.exit(1);
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Shared utility functions for all Doufang skills
|
|
5
|
+
* Provides common functionality to avoid code duplication
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { fileURLToPath } from 'url';
|
|
9
|
+
import { dirname, join, resolve } from 'path';
|
|
10
|
+
import { readFileSync, existsSync, statSync } from 'fs';
|
|
11
|
+
import { execSync } from 'child_process';
|
|
12
|
+
import { createRequire } from 'module';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Get project root directory
|
|
16
|
+
*/
|
|
17
|
+
export function getProjectRoot(importMetaUrl) {
|
|
18
|
+
const __filename = fileURLToPath(importMetaUrl);
|
|
19
|
+
const __dirname = dirname(__filename);
|
|
20
|
+
const skillDir = resolve(__dirname);
|
|
21
|
+
return resolve(skillDir, '../..');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Load environment variables from .env files
|
|
26
|
+
*/
|
|
27
|
+
export async function loadEnvironmentVariables(projectRoot) {
|
|
28
|
+
try {
|
|
29
|
+
const dotenv = await import('dotenv');
|
|
30
|
+
const envLocalPath = join(projectRoot, '.env.local');
|
|
31
|
+
const envPath = join(projectRoot, '.env');
|
|
32
|
+
const cwdEnvLocalPath = join(process.cwd(), '.env.local');
|
|
33
|
+
const cwdEnvPath = join(process.cwd(), '.env');
|
|
34
|
+
|
|
35
|
+
if (existsSync(envLocalPath)) {
|
|
36
|
+
dotenv.config({ path: envLocalPath });
|
|
37
|
+
} else if (existsSync(envPath)) {
|
|
38
|
+
dotenv.config({ path: envPath });
|
|
39
|
+
} else if (existsSync(cwdEnvLocalPath)) {
|
|
40
|
+
dotenv.config({ path: cwdEnvLocalPath });
|
|
41
|
+
} else if (existsSync(cwdEnvPath)) {
|
|
42
|
+
dotenv.config({ path: cwdEnvPath });
|
|
43
|
+
} else {
|
|
44
|
+
dotenv.config();
|
|
45
|
+
}
|
|
46
|
+
} catch (e) {
|
|
47
|
+
// dotenv not available, continue without it (will use environment variables)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Find services directory (supports both compiled dist/ and source services/)
|
|
53
|
+
*/
|
|
54
|
+
export function findServicesPath(projectRoot) {
|
|
55
|
+
// Get npm global prefix to find globally installed packages
|
|
56
|
+
let globalPrefix = null;
|
|
57
|
+
try {
|
|
58
|
+
globalPrefix = execSync('npm config get prefix', { encoding: 'utf-8' }).trim();
|
|
59
|
+
} catch (e) {
|
|
60
|
+
// Ignore error, try other methods
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Try to resolve package location using createRequire (works in ES modules)
|
|
64
|
+
let packageRoot = null;
|
|
65
|
+
try {
|
|
66
|
+
const require = createRequire(import.meta.url);
|
|
67
|
+
const packageJsonPath = require.resolve('@justin_666/square-couplets-master-skills/package.json');
|
|
68
|
+
packageRoot = dirname(packageJsonPath);
|
|
69
|
+
} catch (e) {
|
|
70
|
+
// Package not found via require.resolve, will try other paths
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Build list of possible paths
|
|
74
|
+
// IMPORTANT: We're looking for dist/services (compiled) not services/ (source)
|
|
75
|
+
const possiblePaths = [
|
|
76
|
+
// Global npm package - dist directory (compiled JS)
|
|
77
|
+
...(globalPrefix ? [
|
|
78
|
+
join(globalPrefix, 'lib', 'node_modules', '@justin_666', 'square-couplets-master-skills', 'dist', 'services'),
|
|
79
|
+
join(globalPrefix, 'node_modules', '@justin_666', 'square-couplets-master-skills', 'dist', 'services'),
|
|
80
|
+
] : []),
|
|
81
|
+
// From resolved package root - dist directory
|
|
82
|
+
...(packageRoot ? [
|
|
83
|
+
join(packageRoot, 'dist', 'services'),
|
|
84
|
+
join(packageRoot, 'services'), // fallback to source
|
|
85
|
+
] : []),
|
|
86
|
+
// Local project root - dist directory
|
|
87
|
+
join(projectRoot, 'dist', 'services'),
|
|
88
|
+
join(projectRoot, 'services'), // fallback to source
|
|
89
|
+
// Local node_modules - dist directory
|
|
90
|
+
join(projectRoot, 'node_modules', '@justin_666', 'square-couplets-master-skills', 'dist', 'services'),
|
|
91
|
+
// Current working directory - dist directory
|
|
92
|
+
join(process.cwd(), 'dist', 'services'),
|
|
93
|
+
join(process.cwd(), 'services'),
|
|
94
|
+
// Current working directory node_modules
|
|
95
|
+
join(process.cwd(), 'node_modules', '@justin_666', 'square-couplets-master-skills', 'dist', 'services'),
|
|
96
|
+
];
|
|
97
|
+
|
|
98
|
+
// Debug: log all paths being checked (enable with DEBUG_DOUFANG=1)
|
|
99
|
+
if (process.env.DEBUG_DOUFANG) {
|
|
100
|
+
console.log('🔍 Checking paths for services directory:');
|
|
101
|
+
console.log(` packageRoot: ${packageRoot || 'not found'}`);
|
|
102
|
+
console.log(` globalPrefix: ${globalPrefix || 'not found'}`);
|
|
103
|
+
console.log(` projectRoot: ${projectRoot}`);
|
|
104
|
+
console.log(` cwd: ${process.cwd()}`);
|
|
105
|
+
console.log('\n Trying paths:');
|
|
106
|
+
for (const path of possiblePaths) {
|
|
107
|
+
const exists = existsSync(path);
|
|
108
|
+
console.log(` ${exists ? '✅' : '❌'} ${path}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
for (const path of possiblePaths) {
|
|
113
|
+
try {
|
|
114
|
+
if (statSync(path).isDirectory()) {
|
|
115
|
+
if (process.env.DEBUG_DOUFANG) {
|
|
116
|
+
console.log(`\n✅ Found services at: ${path}`);
|
|
117
|
+
}
|
|
118
|
+
return path;
|
|
119
|
+
}
|
|
120
|
+
} catch (e) {
|
|
121
|
+
// Path doesn't exist, try next
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// If not found, provide helpful error message
|
|
126
|
+
if (process.env.DEBUG_DOUFANG) {
|
|
127
|
+
console.log('\n❌ Services directory not found in any of the checked paths');
|
|
128
|
+
}
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Get MIME type from file extension
|
|
134
|
+
*/
|
|
135
|
+
export function getMimeType(filePath) {
|
|
136
|
+
const ext = filePath.split('.').pop()?.toLowerCase();
|
|
137
|
+
const mimeTypes = {
|
|
138
|
+
'jpg': 'image/jpeg',
|
|
139
|
+
'jpeg': 'image/jpeg',
|
|
140
|
+
'png': 'image/png',
|
|
141
|
+
'gif': 'image/gif',
|
|
142
|
+
'webp': 'image/webp',
|
|
143
|
+
'bmp': 'image/bmp'
|
|
144
|
+
};
|
|
145
|
+
return mimeTypes[ext] || 'image/png'; // default to png
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Get API key from environment variables
|
|
150
|
+
*/
|
|
151
|
+
export function getApiKey() {
|
|
152
|
+
return process.env.GEMINI_API_KEY ||
|
|
153
|
+
process.env.API_KEY ||
|
|
154
|
+
process.env.GOOGLE_GENAI_API_KEY;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Show version from package.json
|
|
159
|
+
*/
|
|
160
|
+
export function showVersion(projectRoot) {
|
|
161
|
+
const packageJsonPath = join(projectRoot, 'package.json');
|
|
162
|
+
if (existsSync(packageJsonPath)) {
|
|
163
|
+
const pkg = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
|
|
164
|
+
console.log(`v${pkg.version}`);
|
|
165
|
+
} else {
|
|
166
|
+
console.log('version unknown');
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Create a progress spinner for long-running operations
|
|
172
|
+
*/
|
|
173
|
+
export function createProgressSpinner(message = 'Processing...') {
|
|
174
|
+
const spinner = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
175
|
+
let spinnerIndex = 0;
|
|
176
|
+
let interval = null;
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
start() {
|
|
180
|
+
interval = setInterval(() => {
|
|
181
|
+
process.stdout.write(`\r${spinner[spinnerIndex]} ${message}`);
|
|
182
|
+
spinnerIndex = (spinnerIndex + 1) % spinner.length;
|
|
183
|
+
}, 80);
|
|
184
|
+
},
|
|
185
|
+
|
|
186
|
+
stop(finalMessage = '') {
|
|
187
|
+
if (interval) {
|
|
188
|
+
clearInterval(interval);
|
|
189
|
+
interval = null;
|
|
190
|
+
}
|
|
191
|
+
if (finalMessage) {
|
|
192
|
+
process.stdout.write(`\r${finalMessage}${' '.repeat(50)}\n`);
|
|
193
|
+
} else {
|
|
194
|
+
process.stdout.write('\r' + ' '.repeat(100) + '\r');
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Load reference image as data URL
|
|
202
|
+
*/
|
|
203
|
+
export function loadReferenceImage(referenceImagePath) {
|
|
204
|
+
const fullPath = resolve(process.cwd(), referenceImagePath);
|
|
205
|
+
if (!existsSync(fullPath)) {
|
|
206
|
+
throw new Error(`Reference image not found: ${fullPath}`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const imageBuffer = readFileSync(fullPath);
|
|
210
|
+
const base64 = imageBuffer.toString('base64');
|
|
211
|
+
const mimeType = getMimeType(referenceImagePath);
|
|
212
|
+
return `data:${mimeType};base64,${base64}`;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Import service module dynamically
|
|
217
|
+
*/
|
|
218
|
+
export async function importServiceModule(servicesPath) {
|
|
219
|
+
const possibleServicePaths = [
|
|
220
|
+
// 1. Compiled JS in dist/ (npm package)
|
|
221
|
+
join(dirname(servicesPath), 'dist', 'services', 'geminiService.js'),
|
|
222
|
+
// 2. Compiled JS in services/ (if built locally)
|
|
223
|
+
join(servicesPath, 'geminiService.js'),
|
|
224
|
+
// 3. Source TS (development only)
|
|
225
|
+
join(servicesPath, 'geminiService.ts'),
|
|
226
|
+
];
|
|
227
|
+
|
|
228
|
+
let importError = null;
|
|
229
|
+
for (const servicePath of possibleServicePaths) {
|
|
230
|
+
try {
|
|
231
|
+
if (existsSync(servicePath)) {
|
|
232
|
+
return await import(`file://${servicePath}`);
|
|
233
|
+
}
|
|
234
|
+
} catch (e) {
|
|
235
|
+
importError = e;
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// If we get here, nothing worked
|
|
241
|
+
const error = new Error('Cannot import service module');
|
|
242
|
+
error.details = {
|
|
243
|
+
triedPaths: possibleServicePaths,
|
|
244
|
+
lastError: importError?.message
|
|
245
|
+
};
|
|
246
|
+
throw error;
|
|
247
|
+
}
|