ginskill-init 1.0.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/README.md +77 -0
- package/agents/developer.md +56 -0
- package/agents/frontend-design.md +69 -0
- package/agents/mobile-reviewer.md +36 -0
- package/agents/review-code.md +49 -0
- package/agents/security-scanner.md +50 -0
- package/agents/tester.md +72 -0
- package/bin/cli.js +226 -0
- package/package.json +20 -0
- package/skills/ai-asset-generator/SKILL.md +255 -0
- package/skills/ai-asset-generator/docs/gen-image.md +274 -0
- package/skills/ai-asset-generator/docs/genvideo.md +341 -0
- package/skills/ai-asset-generator/docs/remove-background.md +19 -0
- package/skills/ai-asset-generator/generate-credit-assets.mjs +180 -0
- package/skills/ai-asset-generator/generate-ginbrowser-assets.mjs +242 -0
- package/skills/ai-asset-generator/generate-sty-icon.mjs +149 -0
- package/skills/ai-asset-generator/lib/bg-remove.mjs +34 -0
- package/skills/ai-asset-generator/lib/env.mjs +38 -0
- package/skills/ai-asset-generator/lib/kie-client.mjs +88 -0
- package/skills/ai-asset-generator/scripts/scaffold-generator.mjs +203 -0
- package/skills/ai-build-ai/SKILL.md +124 -0
- package/skills/ai-build-ai/docs/agent-teams.md +293 -0
- package/skills/ai-build-ai/docs/checkpointing.md +161 -0
- package/skills/ai-build-ai/docs/create-agent.md +399 -0
- package/skills/ai-build-ai/docs/create-mcp.md +395 -0
- package/skills/ai-build-ai/docs/create-skill.md +299 -0
- package/skills/ai-build-ai/docs/headless-mode.md +614 -0
- package/skills/ai-build-ai/docs/hooks.md +578 -0
- package/skills/ai-build-ai/docs/memory-claude-md.md +375 -0
- package/skills/ai-build-ai/docs/output-styles.md +208 -0
- package/skills/ai-build-ai/docs/overview.md +162 -0
- package/skills/ai-build-ai/docs/permissions.md +391 -0
- package/skills/ai-build-ai/docs/plugins.md +396 -0
- package/skills/ai-build-ai/docs/sandbox.md +262 -0
- package/skills/ai-build-ai/scripts/load-tutorial.sh +54 -0
- package/skills/icon-generator/SKILL.md +270 -0
- package/skills/mobile-app-review/SKILL.md +321 -0
- package/skills/mobile-app-review/references/apple-review.md +132 -0
- package/skills/mobile-app-review/references/google-play-review.md +203 -0
- package/skills/mongodb/SKILL.md +667 -0
- package/skills/mongodb/references/mongoose-patterns.md +368 -0
- package/skills/nestjs-architecture/SKILL.md +1086 -0
- package/skills/nestjs-architecture/references/advanced-patterns.md +590 -0
- package/skills/performance/SKILL.md +509 -0
- package/skills/react-fsd-architecture/SKILL.md +693 -0
- package/skills/react-fsd-architecture/references/fsd-patterns.md +747 -0
- package/skills/react-query/SKILL.md +685 -0
- package/skills/react-query/references/query-patterns.md +365 -0
- package/skills/review-code/SKILL.md +321 -0
- package/skills/review-code/references/clean-code-principles.md +395 -0
- package/skills/review-code/references/frontend-patterns.md +136 -0
- package/skills/review-code/references/nestjs-patterns.md +184 -0
- package/skills/review-code/scripts/check-module.sh +201 -0
- package/skills/review-code/scripts/deep-scan.sh +604 -0
- package/skills/review-code/scripts/dep-check.sh +522 -0
- package/skills/review-code/scripts/detect-duplicates.sh +466 -0
- package/skills/review-code/scripts/format-check.sh +577 -0
- package/skills/review-code/scripts/run-review.sh +167 -0
- package/skills/review-code/scripts/scan-codebase.sh +152 -0
- package/skills/security-scanner/SKILL.md +327 -0
- package/skills/security-scanner/references/nestjs-security.md +260 -0
- package/skills/security-scanner/references/nextjs-security.md +201 -0
- package/skills/security-scanner/references/react-native-security.md +199 -0
- package/skills/security-scanner/scripts/security-scan.sh +478 -0
- package/skills/ui-ux-pro-max/SKILL.md +377 -0
- package/skills/ui-ux-pro-max/data/charts.csv +26 -0
- package/skills/ui-ux-pro-max/data/colors.csv +97 -0
- package/skills/ui-ux-pro-max/data/icons.csv +101 -0
- package/skills/ui-ux-pro-max/data/landing.csv +31 -0
- package/skills/ui-ux-pro-max/data/products.csv +97 -0
- package/skills/ui-ux-pro-max/data/react-performance.csv +45 -0
- package/skills/ui-ux-pro-max/data/stacks/astro.csv +54 -0
- package/skills/ui-ux-pro-max/data/stacks/flutter.csv +53 -0
- package/skills/ui-ux-pro-max/data/stacks/html-tailwind.csv +56 -0
- package/skills/ui-ux-pro-max/data/stacks/jetpack-compose.csv +53 -0
- package/skills/ui-ux-pro-max/data/stacks/nextjs.csv +53 -0
- package/skills/ui-ux-pro-max/data/stacks/nuxt-ui.csv +51 -0
- package/skills/ui-ux-pro-max/data/stacks/nuxtjs.csv +59 -0
- package/skills/ui-ux-pro-max/data/stacks/react-native.csv +52 -0
- package/skills/ui-ux-pro-max/data/stacks/react.csv +54 -0
- package/skills/ui-ux-pro-max/data/stacks/shadcn.csv +61 -0
- package/skills/ui-ux-pro-max/data/stacks/svelte.csv +54 -0
- package/skills/ui-ux-pro-max/data/stacks/swiftui.csv +51 -0
- package/skills/ui-ux-pro-max/data/stacks/vue.csv +50 -0
- package/skills/ui-ux-pro-max/data/styles.csv +68 -0
- package/skills/ui-ux-pro-max/data/typography.csv +58 -0
- package/skills/ui-ux-pro-max/data/ui-reasoning.csv +101 -0
- package/skills/ui-ux-pro-max/data/ux-guidelines.csv +100 -0
- package/skills/ui-ux-pro-max/data/web-interface.csv +31 -0
- package/skills/ui-ux-pro-max/scripts/core.py +253 -0
- package/skills/ui-ux-pro-max/scripts/design_system.py +1067 -0
- package/skills/ui-ux-pro-max/scripts/search.py +114 -0
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* GinBrowser Asset Generator
|
|
5
|
+
*
|
|
6
|
+
* Generates hero images, feature illustrations, and hero background video
|
|
7
|
+
* using the KIE AI API (nano-banana-pro for images, bytedance for video).
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* node tools/generate-ginbrowser-assets.mjs # generate all
|
|
11
|
+
* node tools/generate-ginbrowser-assets.mjs --images # images only
|
|
12
|
+
* node tools/generate-ginbrowser-assets.mjs --video <url> # video only
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { join, dirname } from "path";
|
|
16
|
+
import { fileURLToPath } from "url";
|
|
17
|
+
import { loadEnv } from "./lib/env.mjs";
|
|
18
|
+
import { createTask, pollUntilDone, downloadFile, log } from "./lib/kie-client.mjs";
|
|
19
|
+
|
|
20
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
21
|
+
const ROOT = join(__dirname, "..");
|
|
22
|
+
const OUTPUT_DIR = join(ROOT, "public", "images");
|
|
23
|
+
|
|
24
|
+
// ─── Prompt Engineering ─────────────────────────────────────
|
|
25
|
+
const IMAGE_ASSETS = [
|
|
26
|
+
{
|
|
27
|
+
name: "hero-bg",
|
|
28
|
+
filename: "hero-bg.png",
|
|
29
|
+
aspect_ratio: "16:9",
|
|
30
|
+
resolution: "2K",
|
|
31
|
+
prompt: [
|
|
32
|
+
"Cinematic ultra-wide establishing shot of a dark digital fortress environment.",
|
|
33
|
+
"Pure black void (#0A0E1A) as the base.",
|
|
34
|
+
"In the center, a massive translucent shield construct made of hexagonal grid panels",
|
|
35
|
+
"glowing with soft emerald (#10B981) and cyan (#06B6D4) light,",
|
|
36
|
+
"surrounded by orbiting data streams and encrypted packet visualizations.",
|
|
37
|
+
"Thousands of tiny luminous particles form fingerprint-like patterns",
|
|
38
|
+
"that dissolve into digital noise and scatter outward.",
|
|
39
|
+
"Multiple browser window silhouettes float in the background,",
|
|
40
|
+
"each with slightly different configurations, representing multiple identities.",
|
|
41
|
+
"Soft volumetric emerald light radiates from behind the central shield.",
|
|
42
|
+
"Center has ample dark negative space for UI overlay.",
|
|
43
|
+
"Ultra high quality photorealistic 3D render with volumetric lighting and depth of field.",
|
|
44
|
+
"Absolutely no text, no typography, no logos, no people, no UI elements, no watermarks.",
|
|
45
|
+
"Dark moody atmosphere, premium cybersecurity aesthetic.",
|
|
46
|
+
].join(" "),
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
name: "feature-fingerprint",
|
|
50
|
+
filename: "feature-fingerprint.png",
|
|
51
|
+
aspect_ratio: "1:1",
|
|
52
|
+
resolution: "1K",
|
|
53
|
+
prompt: [
|
|
54
|
+
"Abstract premium technology illustration on a pure black background.",
|
|
55
|
+
"A large translucent holographic fingerprint floating in dark space,",
|
|
56
|
+
"constructed from glowing emerald (#10B981) digital circuit traces",
|
|
57
|
+
"and fine data-stream lines with tiny pulsing encrypted nodes.",
|
|
58
|
+
"The fingerprint gradually dissolves into scattered pixels and noise",
|
|
59
|
+
"on the right side, symbolizing digital fingerprint masking.",
|
|
60
|
+
"Tiny shield icons and lock symbols orbit around the fingerprint.",
|
|
61
|
+
"Clean minimal composition with generous dark breathing room.",
|
|
62
|
+
"Emerald and cyan accent lighting on pure black.",
|
|
63
|
+
"Ultra high quality 3D render. Photorealistic volumetric light.",
|
|
64
|
+
"No text, no typography, no logos, no people, no watermarks.",
|
|
65
|
+
"Premium cybersecurity illustration style.",
|
|
66
|
+
].join(" "),
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
name: "feature-profiles",
|
|
70
|
+
filename: "feature-profiles.png",
|
|
71
|
+
aspect_ratio: "1:1",
|
|
72
|
+
resolution: "1K",
|
|
73
|
+
prompt: [
|
|
74
|
+
"Abstract premium technology illustration on a pure black background.",
|
|
75
|
+
"Multiple floating translucent browser window shapes arranged in a fan formation,",
|
|
76
|
+
"each glowing with different subtle color tints - emerald, cyan, blue, teal.",
|
|
77
|
+
"Each browser window has a unique abstract avatar silhouette inside it.",
|
|
78
|
+
"Thin luminous connection threads link the windows to a central hub node.",
|
|
79
|
+
"The windows have frosted glass appearance with soft inner glow.",
|
|
80
|
+
"Tiny data particles flow between the windows and the central node.",
|
|
81
|
+
"Clean minimal composition with generous negative space.",
|
|
82
|
+
"Emerald and teal accent colors on pure black.",
|
|
83
|
+
"Ultra high quality 3D render. Photorealistic volumetric light.",
|
|
84
|
+
"No text, no typography, no logos, no people, no watermarks.",
|
|
85
|
+
"Premium multi-identity management illustration style.",
|
|
86
|
+
].join(" "),
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
name: "feature-stealth",
|
|
90
|
+
filename: "feature-stealth.png",
|
|
91
|
+
aspect_ratio: "1:1",
|
|
92
|
+
resolution: "1K",
|
|
93
|
+
prompt: [
|
|
94
|
+
"Abstract premium technology illustration on a pure black background.",
|
|
95
|
+
"A crystalline transparent eye shape floating in dark space,",
|
|
96
|
+
"constructed from hundreds of tiny emerald (#10B981) and cyan data points.",
|
|
97
|
+
"The eye is crossed out by an elegant glowing slash line,",
|
|
98
|
+
"symbolizing undetectable browsing and anti-tracking.",
|
|
99
|
+
"Around the eye, scattered detection radar waves dissolve into noise.",
|
|
100
|
+
"Faint grid lines recede into the background suggesting the web.",
|
|
101
|
+
"Small shield fragments and pixel dust float around the composition.",
|
|
102
|
+
"Clean minimal composition with generous negative space.",
|
|
103
|
+
"Emerald and cyan accent colors on pure black.",
|
|
104
|
+
"Ultra high quality 3D render. Photorealistic volumetric light.",
|
|
105
|
+
"No text, no typography, no logos, no people, no watermarks.",
|
|
106
|
+
"Premium anti-detection stealth illustration style.",
|
|
107
|
+
].join(" "),
|
|
108
|
+
},
|
|
109
|
+
];
|
|
110
|
+
|
|
111
|
+
const VIDEO_PROMPT = [
|
|
112
|
+
"Extremely slow, hypnotic forward camera drift through the scene.",
|
|
113
|
+
"The hexagonal shield panels pulse gently with soft emerald bioluminescent light in slow waves.",
|
|
114
|
+
"Fingerprint particle patterns continuously dissolve and reform in mesmerizing cycles.",
|
|
115
|
+
"Translucent encrypted data streams shimmer and slowly orbit the central shield.",
|
|
116
|
+
"Microscopic particles drift past the lens creating dreamy bokeh.",
|
|
117
|
+
"Volumetric fog shifts almost imperceptibly in the background.",
|
|
118
|
+
"Ultra smooth, meditative cinematic motion.",
|
|
119
|
+
"No sudden movements, no flashes, no camera shake.",
|
|
120
|
+
"Premium atmospheric cybersecurity ambience.",
|
|
121
|
+
].join(" ");
|
|
122
|
+
|
|
123
|
+
// ─── Main ───────────────────────────────────────────────────
|
|
124
|
+
async function generateImages() {
|
|
125
|
+
log("\n--- Phase 1: Submitting image tasks ---\n");
|
|
126
|
+
|
|
127
|
+
const jobs = await Promise.all(
|
|
128
|
+
IMAGE_ASSETS.map(async (asset) => {
|
|
129
|
+
log(` [submit] ${asset.name} (${asset.aspect_ratio} ${asset.resolution})`);
|
|
130
|
+
const taskId = await createTask({
|
|
131
|
+
model: "nano-banana-pro",
|
|
132
|
+
input: {
|
|
133
|
+
prompt: asset.prompt,
|
|
134
|
+
aspect_ratio: asset.aspect_ratio,
|
|
135
|
+
resolution: asset.resolution,
|
|
136
|
+
output_format: "png",
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
log(` [queued] ${asset.name} taskId=${taskId}`);
|
|
140
|
+
return { ...asset, taskId };
|
|
141
|
+
})
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
log(`\n--- Phase 2: Polling ${jobs.length} image tasks ---\n`);
|
|
145
|
+
|
|
146
|
+
const results = await Promise.all(
|
|
147
|
+
jobs.map(async (job) => {
|
|
148
|
+
const url = await pollUntilDone(job.taskId, job.name);
|
|
149
|
+
return { ...job, resultUrl: url };
|
|
150
|
+
})
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
log("\n--- Phase 3: Downloading images ---\n");
|
|
154
|
+
|
|
155
|
+
for (const r of results) {
|
|
156
|
+
await downloadFile(r.resultUrl, join(OUTPUT_DIR, r.filename));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return results;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function generateVideo(heroImageUrl) {
|
|
163
|
+
log("\n--- Phase 4: Submitting video task ---\n");
|
|
164
|
+
|
|
165
|
+
log(` [submit] hero-bg-video (720p 5s)`);
|
|
166
|
+
log(` [source] ${heroImageUrl}`);
|
|
167
|
+
|
|
168
|
+
const taskId = await createTask({
|
|
169
|
+
model: "bytedance/v1-pro-image-to-video",
|
|
170
|
+
input: {
|
|
171
|
+
prompt: VIDEO_PROMPT,
|
|
172
|
+
image_url: heroImageUrl,
|
|
173
|
+
resolution: "720p",
|
|
174
|
+
duration: "5",
|
|
175
|
+
camera_fixed: false,
|
|
176
|
+
seed: -1,
|
|
177
|
+
enable_safety_checker: true,
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
log(` [queued] hero-bg-video taskId=${taskId}`);
|
|
182
|
+
log("\n--- Phase 5: Polling video task (may take several minutes) ---\n");
|
|
183
|
+
|
|
184
|
+
const videoUrl = await pollUntilDone(taskId, "hero-bg-video", 600_000);
|
|
185
|
+
|
|
186
|
+
log("\n--- Phase 6: Downloading video ---\n");
|
|
187
|
+
await downloadFile(videoUrl, join(OUTPUT_DIR, "hero-bg.mp4"));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async function main() {
|
|
191
|
+
await loadEnv();
|
|
192
|
+
|
|
193
|
+
if (!process.env.KIE_AI_API_KEY) {
|
|
194
|
+
console.error("Error: KIE_AI_API_KEY not found. Check .env or .env.local");
|
|
195
|
+
process.exit(1);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const args = process.argv.slice(2);
|
|
199
|
+
const imagesOnly = args.includes("--images");
|
|
200
|
+
const videoOnly = args.includes("--video");
|
|
201
|
+
|
|
202
|
+
console.log("\n========================================");
|
|
203
|
+
console.log(" GinBrowser Asset Generator");
|
|
204
|
+
console.log("========================================");
|
|
205
|
+
|
|
206
|
+
if (videoOnly) {
|
|
207
|
+
const urlArg = args.find((a) => a.startsWith("http"));
|
|
208
|
+
if (!urlArg) {
|
|
209
|
+
console.error("Video-only mode requires a hero image URL argument.");
|
|
210
|
+
console.error("Usage: node tools/generate-ginbrowser-assets.mjs --video <hero-image-url>");
|
|
211
|
+
process.exit(1);
|
|
212
|
+
}
|
|
213
|
+
await generateVideo(urlArg);
|
|
214
|
+
} else {
|
|
215
|
+
const results = await generateImages();
|
|
216
|
+
|
|
217
|
+
if (!imagesOnly) {
|
|
218
|
+
const heroResult = results.find((r) => r.name === "hero-bg");
|
|
219
|
+
if (heroResult?.resultUrl) {
|
|
220
|
+
await generateVideo(heroResult.resultUrl);
|
|
221
|
+
} else {
|
|
222
|
+
log("\n [skip] Video generation: hero-bg image not available");
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
console.log("\n========================================");
|
|
228
|
+
console.log(" All done!");
|
|
229
|
+
console.log("========================================");
|
|
230
|
+
console.log("\nGenerated assets in public/images/:");
|
|
231
|
+
console.log(" - hero-bg.png (hero background)");
|
|
232
|
+
console.log(" - hero-bg.mp4 (hero video)");
|
|
233
|
+
console.log(" - feature-fingerprint.png (fingerprint masking)");
|
|
234
|
+
console.log(" - feature-profiles.png (multi-profile management)");
|
|
235
|
+
console.log(" - feature-stealth.png (stealth/anti-detection)");
|
|
236
|
+
console.log("");
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
main().catch((err) => {
|
|
240
|
+
console.error(`\nFatal: ${err.message}`);
|
|
241
|
+
process.exit(1);
|
|
242
|
+
});
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Sty Credit Icon Generator
|
|
5
|
+
*
|
|
6
|
+
* Generates ONE unified credit/coin icon for the entire app.
|
|
7
|
+
* This icon replaces the inconsistent SVG StyIcon + coin-reward.png.
|
|
8
|
+
*
|
|
9
|
+
* Pipeline:
|
|
10
|
+
* 1. Generate via KIE AI API (nano-banana-pro)
|
|
11
|
+
* 2. Remove background via Sty AI API
|
|
12
|
+
* 3. Convert to WebP via ffmpeg (lightweight)
|
|
13
|
+
* 4. Output multiple sizes: @1x (24px), @2x (48px), @3x (72px), hero (160px)
|
|
14
|
+
*
|
|
15
|
+
* Usage:
|
|
16
|
+
* node tools/generate-sty-icon.mjs
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { writeFile, readFile } from "fs/promises";
|
|
20
|
+
import { join, dirname } from "path";
|
|
21
|
+
import { fileURLToPath } from "url";
|
|
22
|
+
import { execSync } from "child_process";
|
|
23
|
+
import { loadEnv } from "./lib/env.mjs";
|
|
24
|
+
import { createTask, pollUntilDone, downloadFile, log } from "./lib/kie-client.mjs";
|
|
25
|
+
import { removeBackground } from "./lib/bg-remove.mjs";
|
|
26
|
+
|
|
27
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
28
|
+
const OUTPUT_DIR = join(__dirname, "..", "styai-mobile", "src", "assets", "images", "credits");
|
|
29
|
+
|
|
30
|
+
// ─── ffmpeg helpers ─────────────────────────────────────────
|
|
31
|
+
function hasFFmpeg() {
|
|
32
|
+
try { execSync("which ffmpeg", { stdio: "pipe" }); return true; } catch { return false; }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function ffmpegResize(input, output, size) {
|
|
36
|
+
execSync(`ffmpeg -y -i "${input}" -vf "scale=${size}:${size}:flags=lanczos" "${output}"`, { stdio: "pipe" });
|
|
37
|
+
log(` [resized] ${output} (${size}x${size})`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function ffmpegToWebP(input, output) {
|
|
41
|
+
try {
|
|
42
|
+
execSync(`ffmpeg -y -i "${input}" -c:v libwebp -quality 90 -lossless 0 "${output}"`, { stdio: "pipe" });
|
|
43
|
+
log(` [webp] ${output}`);
|
|
44
|
+
} catch {
|
|
45
|
+
log(` [skip] WebP not available, keeping PNG: ${input}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ─── Main ───────────────────────────────────────────────────
|
|
50
|
+
async function main() {
|
|
51
|
+
await loadEnv();
|
|
52
|
+
|
|
53
|
+
if (!process.env.KIE_AI_API_KEY) {
|
|
54
|
+
console.error("Error: KIE_AI_API_KEY not found. Check .env");
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const useFFmpeg = hasFFmpeg();
|
|
59
|
+
if (!useFFmpeg) {
|
|
60
|
+
log("[warn] ffmpeg not found — skipping resize and WebP conversion");
|
|
61
|
+
log("[warn] Install with: brew install ffmpeg");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
console.log("\n========================================");
|
|
65
|
+
console.log(" Sty Credit Icon Generator");
|
|
66
|
+
console.log(" One icon to rule them all");
|
|
67
|
+
console.log("========================================\n");
|
|
68
|
+
|
|
69
|
+
// ─── Phase 1: Generate high-res icon ──────────────────────
|
|
70
|
+
const prompt = [
|
|
71
|
+
"A single flat coin icon on a solid pure white #FFFFFF background, centered, no shadows on background.",
|
|
72
|
+
"The coin is a perfect circle, viewed straight-on (flat, no tilt, no perspective).",
|
|
73
|
+
"Coin color: vibrant rose-pink gradient from #EC4899 to #DB2777, smooth metallic finish.",
|
|
74
|
+
"On the coin face: a bold clean letter S in white, centered, modern sans-serif font, slightly embossed.",
|
|
75
|
+
"The coin has a thin clean rim, slightly lighter rose-pink #F87198.",
|
|
76
|
+
"One subtle diagonal highlight across the coin for a polished metallic look.",
|
|
77
|
+
"NO particles, NO sparkles, NO glow, NO halo, NO floating elements around the coin.",
|
|
78
|
+
"NO shadows on the background. The coin edges must be crisp and clean against the white background.",
|
|
79
|
+
"Solid pure white background only. Clean, minimal, flat icon style.",
|
|
80
|
+
"High quality digital illustration, app icon style, crisp vector-like rendering.",
|
|
81
|
+
"No text besides the S. No watermarks, no people, no extra elements.",
|
|
82
|
+
].join(" ");
|
|
83
|
+
|
|
84
|
+
log("--- Phase 1: Generating high-res coin icon ---\n");
|
|
85
|
+
log(" [submit] sty-coin (1:1 2K)");
|
|
86
|
+
|
|
87
|
+
const taskId = await createTask({
|
|
88
|
+
model: "nano-banana-pro",
|
|
89
|
+
input: {
|
|
90
|
+
prompt,
|
|
91
|
+
aspect_ratio: "1:1",
|
|
92
|
+
resolution: "2K",
|
|
93
|
+
output_format: "png",
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
log(` [queued] sty-coin taskId=${taskId}`);
|
|
98
|
+
log("\n--- Phase 2: Polling ---\n");
|
|
99
|
+
|
|
100
|
+
const resultUrl = await pollUntilDone(taskId, "sty-coin");
|
|
101
|
+
|
|
102
|
+
// ─── Phase 3: Download ────────────────────────────────────
|
|
103
|
+
log("\n--- Phase 3: Downloading ---\n");
|
|
104
|
+
const rawPath = join(OUTPUT_DIR, "sty-coin-raw.png");
|
|
105
|
+
await downloadFile(resultUrl, rawPath);
|
|
106
|
+
|
|
107
|
+
// ─── Phase 4: Remove background ──────────────────────────
|
|
108
|
+
log("\n--- Phase 4: Removing background ---\n");
|
|
109
|
+
const noBgPath = join(OUTPUT_DIR, "sty-coin.png");
|
|
110
|
+
try {
|
|
111
|
+
await removeBackground(rawPath, noBgPath);
|
|
112
|
+
} catch (err) {
|
|
113
|
+
log(` [warn] Background removal failed: ${err.message}`);
|
|
114
|
+
log(" [fallback] Using raw image as sty-coin.png");
|
|
115
|
+
await writeFile(noBgPath, await readFile(rawPath));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ─── Phase 5: Generate sizes + WebP via ffmpeg ────────────
|
|
119
|
+
if (useFFmpeg) {
|
|
120
|
+
log("\n--- Phase 5: Generating sizes + WebP ---\n");
|
|
121
|
+
|
|
122
|
+
ffmpegResize(noBgPath, join(OUTPUT_DIR, "sty-coin-hero.png"), 160);
|
|
123
|
+
ffmpegResize(noBgPath, join(OUTPUT_DIR, "sty-coin@3x.png"), 72);
|
|
124
|
+
ffmpegResize(noBgPath, join(OUTPUT_DIR, "sty-coin@2x.png"), 48);
|
|
125
|
+
|
|
126
|
+
// WebP versions for lightweight loading
|
|
127
|
+
ffmpegToWebP(join(OUTPUT_DIR, "sty-coin-hero.png"), join(OUTPUT_DIR, "sty-coin-hero.webp"));
|
|
128
|
+
ffmpegToWebP(join(OUTPUT_DIR, "sty-coin@3x.png"), join(OUTPUT_DIR, "sty-coin@3x.webp"));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
console.log("\n========================================");
|
|
132
|
+
console.log(" All done!");
|
|
133
|
+
console.log("========================================");
|
|
134
|
+
console.log(`\nGenerated in ${OUTPUT_DIR}:`);
|
|
135
|
+
console.log(" - sty-coin.png (full-res, transparent bg)");
|
|
136
|
+
if (useFFmpeg) {
|
|
137
|
+
console.log(" - sty-coin@2x.png (48px)");
|
|
138
|
+
console.log(" - sty-coin@3x.png (72px)");
|
|
139
|
+
console.log(" - sty-coin-hero.png (160px, for hero section)");
|
|
140
|
+
console.log(" - sty-coin-hero.webp (160px, lightweight)");
|
|
141
|
+
console.log(" - sty-coin@3x.webp (72px, lightweight)");
|
|
142
|
+
}
|
|
143
|
+
console.log("");
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
main().catch((err) => {
|
|
147
|
+
console.error(`\nFatal: ${err.message}`);
|
|
148
|
+
process.exit(1);
|
|
149
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared background removal via Sty AI API.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { writeFile, readFile } from "fs/promises";
|
|
6
|
+
import { log } from "./kie-client.mjs";
|
|
7
|
+
|
|
8
|
+
export async function removeBackground(inputPath, outputPath) {
|
|
9
|
+
const fileBuffer = await readFile(inputPath);
|
|
10
|
+
const blob = new Blob([fileBuffer], { type: "image/png" });
|
|
11
|
+
|
|
12
|
+
const formData = new FormData();
|
|
13
|
+
formData.append("file", blob, "image.png");
|
|
14
|
+
formData.append("cropToForeground", "true");
|
|
15
|
+
formData.append("outputFormat", "png");
|
|
16
|
+
|
|
17
|
+
const res = await fetch("https://api.styai.app/api/v1/media/remove-background", {
|
|
18
|
+
method: "POST",
|
|
19
|
+
headers: {
|
|
20
|
+
accept: "application/json",
|
|
21
|
+
"X-API-Key": "pat_lJ8bAVjHn2_moLH1zxdTm9NnU8rvUcw8HyiQtuHg",
|
|
22
|
+
},
|
|
23
|
+
body: formData,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const json = await res.json();
|
|
27
|
+
if (!json.sucess && !json.success) {
|
|
28
|
+
throw new Error("Background removal failed");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const buffer = Buffer.from(json.buffer, "base64");
|
|
32
|
+
await writeFile(outputPath, buffer);
|
|
33
|
+
log(` [bg-removed] ${outputPath}`);
|
|
34
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared .env loader for ai-asset-generator scripts.
|
|
3
|
+
* Looks for .env in ai-asset-generator/ first, then workspace root.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { readFile } from "fs/promises";
|
|
7
|
+
import { join, dirname } from "path";
|
|
8
|
+
import { fileURLToPath } from "url";
|
|
9
|
+
|
|
10
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
const GENERATOR_DIR = join(__dirname, "..");
|
|
12
|
+
const ROOT = join(__dirname, "..", "..", "..", "..");
|
|
13
|
+
|
|
14
|
+
export async function loadEnv() {
|
|
15
|
+
// Look in ai-asset-generator/ first, then workspace root as fallback
|
|
16
|
+
const searchPaths = [GENERATOR_DIR, ROOT];
|
|
17
|
+
const envFiles = [".env", ".env.local"];
|
|
18
|
+
|
|
19
|
+
for (const dir of searchPaths) {
|
|
20
|
+
for (const envFile of envFiles) {
|
|
21
|
+
try {
|
|
22
|
+
const raw = await readFile(join(dir, envFile), "utf8");
|
|
23
|
+
for (const line of raw.split("\n")) {
|
|
24
|
+
const trimmed = line.trim();
|
|
25
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
26
|
+
const eq = trimmed.indexOf("=");
|
|
27
|
+
if (eq === -1) continue;
|
|
28
|
+
const key = trimmed.slice(0, eq).trim();
|
|
29
|
+
const val = trimmed.slice(eq + 1).trim();
|
|
30
|
+
if (!process.env[key]) process.env[key] = val;
|
|
31
|
+
}
|
|
32
|
+
console.log(` [env] Loaded ${envFile}`);
|
|
33
|
+
} catch {
|
|
34
|
+
// File may not exist, continue
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared KIE AI API client.
|
|
3
|
+
*
|
|
4
|
+
* Provides: createTask, queryTask, pollUntilDone, downloadFile
|
|
5
|
+
* All scripts use the same API base and auth pattern.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { writeFile, mkdir } from "fs/promises";
|
|
9
|
+
import { existsSync } from "fs";
|
|
10
|
+
import { dirname } from "path";
|
|
11
|
+
|
|
12
|
+
// ─── Helpers ────────────────────────────────────────────────
|
|
13
|
+
export const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
14
|
+
export const log = (msg) => console.log(msg);
|
|
15
|
+
|
|
16
|
+
// ─── API Client ─────────────────────────────────────────────
|
|
17
|
+
const API = "https://api.kie.ai/api/v1/jobs";
|
|
18
|
+
|
|
19
|
+
function headers() {
|
|
20
|
+
return {
|
|
21
|
+
"Content-Type": "application/json",
|
|
22
|
+
Authorization: `Bearer ${process.env.KIE_AI_API_KEY}`,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function createTask(payload) {
|
|
27
|
+
const res = await fetch(`${API}/createTask`, {
|
|
28
|
+
method: "POST",
|
|
29
|
+
headers: headers(),
|
|
30
|
+
body: JSON.stringify(payload),
|
|
31
|
+
});
|
|
32
|
+
const json = await res.json();
|
|
33
|
+
if (json.code !== 200) {
|
|
34
|
+
throw new Error(`createTask failed (${json.code}): ${json.message}`);
|
|
35
|
+
}
|
|
36
|
+
return json.data.taskId;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function queryTask(taskId) {
|
|
40
|
+
const res = await fetch(`${API}/recordInfo?taskId=${taskId}`, {
|
|
41
|
+
headers: { Authorization: `Bearer ${process.env.KIE_AI_API_KEY}` },
|
|
42
|
+
});
|
|
43
|
+
const json = await res.json();
|
|
44
|
+
if (json.code !== 200) {
|
|
45
|
+
throw new Error(`queryTask failed (${json.code}): ${json.message}`);
|
|
46
|
+
}
|
|
47
|
+
return json.data;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Poll a task until it reaches success/fail.
|
|
52
|
+
* Uses increasing delay: 5s → 8s → 12s → … capped at 30s.
|
|
53
|
+
*/
|
|
54
|
+
export async function pollUntilDone(taskId, label, timeoutMs = 300_000) {
|
|
55
|
+
const t0 = Date.now();
|
|
56
|
+
let delay = 5_000;
|
|
57
|
+
|
|
58
|
+
while (Date.now() - t0 < timeoutMs) {
|
|
59
|
+
const task = await queryTask(taskId);
|
|
60
|
+
|
|
61
|
+
if (task.state === "success") {
|
|
62
|
+
const urls = JSON.parse(task.resultJson).resultUrls;
|
|
63
|
+
log(` [done] ${label}`);
|
|
64
|
+
return urls[0];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (task.state === "fail") {
|
|
68
|
+
throw new Error(`${label} failed: ${task.failMsg || "unknown"}`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const elapsed = Math.round((Date.now() - t0) / 1000);
|
|
72
|
+
log(` [${task.state}] ${label} (${elapsed}s, next poll in ${Math.round(delay / 1000)}s)`);
|
|
73
|
+
await sleep(delay);
|
|
74
|
+
delay = Math.min(Math.round(delay * 1.5), 30_000);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
throw new Error(`${label} timed out after ${timeoutMs / 1000}s`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function downloadFile(url, dest) {
|
|
81
|
+
const dir = dirname(dest);
|
|
82
|
+
if (!existsSync(dir)) await mkdir(dir, { recursive: true });
|
|
83
|
+
const res = await fetch(url);
|
|
84
|
+
if (!res.ok) throw new Error(`Download failed: ${res.status} ${url}`);
|
|
85
|
+
const buf = Buffer.from(await res.arrayBuffer());
|
|
86
|
+
await writeFile(dest, buf);
|
|
87
|
+
log(` [saved] ${dest}`);
|
|
88
|
+
}
|