Update skills: add website-creator, mql-developer, ecommerce-astro
Changes: - Add FAL_KEY and GEMINI_API_KEY to .env.example - Update picture-it to use ~/.config/opencode/.env (unified creds) - Remove shodh-memory skill (no longer used) - Remove alphaear-* skills (deprecated) - Remove thai-frontend-dev skill (replaced by website-creator) - Remove theme-factory skill - Add mql-developer skill (MQL5 trading) - Add ecommerce-astro skill (Astro e-commerce) - Add website-creator skill (Next.js + Payload CMS) - Update install script for new skills
This commit is contained in:
BIN
skills/website-creator/creative/.DS_Store
vendored
Normal file
BIN
skills/website-creator/creative/.DS_Store
vendored
Normal file
Binary file not shown.
139
skills/website-creator/creative/picture-it/SKILL.md
Normal file
139
skills/website-creator/creative/picture-it/SKILL.md
Normal file
@@ -0,0 +1,139 @@
|
||||
---
|
||||
name: picture-it
|
||||
description: CLI tool for AI image generation and editing using FAL AI. Chainable image operations from simple commands. Supports Thai text rendering after font patch. Use for: generate, edit, remove-bg, replace-bg, crop, grade, text, compose, template, pipeline, batch. Cost-aware: flux-schnell $0.003, banana-pro $0.15.
|
||||
category: creative
|
||||
tags: [image-generation, image-editing, fal-ai, cli, design, banner, social-media, thai]
|
||||
created: 2026-04-08
|
||||
updated: 2026-04-08
|
||||
version: 1.0.0
|
||||
skill_path: ~/.hermes/skills/website-creator/creative/picture-it
|
||||
platforms: [cli, telegram, discord]
|
||||
credentials: ~/.config/opencode/.env
|
||||
maintenance: Thai font patch must be re-applied after every picture-it update
|
||||
---
|
||||
|
||||
# Picture-it Skill
|
||||
|
||||
CLI tool for AI image generation and editing using FAL AI. Chainable image operations with free local processing (crop, grade, grain, vignette) and paid AI operations.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Load credentials first
|
||||
set -a && source ~/.config/opencode/.env && set +a
|
||||
|
||||
# Ensure bun is in PATH
|
||||
export PATH="/home/kunthawat/snap/bun-js/87/.bun/bin:$PATH"
|
||||
|
||||
# Basic generation
|
||||
picture-it generate --prompt "dark cosmic background" --size 1200x630 -o bg.png
|
||||
|
||||
# AI edit
|
||||
picture-it edit -i photo.jpg --prompt "replace background" --model kontext -o edited.jpg
|
||||
|
||||
# Thai text (requires patch)
|
||||
picture-it text -i bg.png --title "ทดสอบภาษาไทย" --font "Kanit" --font-size 64 -o out.png
|
||||
```
|
||||
|
||||
## Commands
|
||||
|
||||
| Command | Description | FAL? | Cost |
|
||||
|---|---|---|---|
|
||||
| `generate` | Text-to-image | Yes | $0.003–$0.25 |
|
||||
| `edit` | AI image editing | Yes | $0.02–$0.15 |
|
||||
| `remove-bg` | Background removal | Yes | free |
|
||||
| `replace-bg` | Remove + generate new bg | Yes | varies |
|
||||
| `crop` | Resize/crop | No | free |
|
||||
| `grade` | Color grading | No | free |
|
||||
| `grain` | Film grain | No | free |
|
||||
| `vignette` | Edge darkening | No | free |
|
||||
| `text` | Render text (Satori) | No | free |
|
||||
| `compose` | JSON overlay | No | free |
|
||||
| `template` | Built-in templates | No | free |
|
||||
| `pipeline` | Multi-step chain | — | varies |
|
||||
| `batch` | Multiple pipelines | — | varies |
|
||||
| `upscale` | AI upscale | Yes | varies |
|
||||
| `info` | Image analysis | No | free |
|
||||
|
||||
## Model Selection
|
||||
|
||||
| Task | Model | Cost |
|
||||
|---|---|---|
|
||||
| Fast draft | `flux-schnell` | $0.003 |
|
||||
| Quality hero | `flux-dev` | $0.03 |
|
||||
| Text in image | `recraft-v3` | $0.04 |
|
||||
| Quick edit | `reve-fast` | $0.02 |
|
||||
| Targeted edit | `kontext` | $0.04 |
|
||||
| Multi-image (≤10) | `seedream` | $0.04 |
|
||||
| Best preservation | `banana2` | $0.08 |
|
||||
| Premium realism | `banana-pro` | $0.15 |
|
||||
|
||||
**Always plan before generating.** A 4-pass workflow is $0.10+.
|
||||
|
||||
## Thai Font System
|
||||
|
||||
picture-it uses Satori for text rendering. Thai fonts are NOT in the default `FONT_FILES` array.
|
||||
|
||||
**Symptom:** Thai text shows as ▢ boxes.
|
||||
**Fix:**
|
||||
```bash
|
||||
bun ~/.hermes/skills/website-creator/creative/picture-it/scripts/thai-font-patch.ts --force
|
||||
```
|
||||
|
||||
**This breaks after every `picture-it` update.** Re-run after updates.
|
||||
|
||||
Thai fonts: `Kanit` (modern, bold), `Noto Sans Thai` (clean, official).
|
||||
|
||||
## Setup
|
||||
|
||||
1. **Install:** `bun install -g picture-it`
|
||||
2. **PATH:** Add `~/snap/bun-js/87/.bun/bin` to `$PATH`
|
||||
3. **FAL_KEY:** Store in `~/.config/opencode/.env` as `FAL_KEY=your_key`
|
||||
4. **Fonts:** `picture-it download-fonts`
|
||||
5. **Thai patch:** `bun ~/.hermes/skills/website-creator/creative/picture-it/scripts/thai-font-patch.ts`
|
||||
|
||||
## Workflow Templates
|
||||
|
||||
### Blog Hero (~$0.04)
|
||||
```
|
||||
1. generate flux-schnell ($0.003) → dark background
|
||||
2. edit seedream ($0.04) → place logo
|
||||
3. grade cinematic (free)
|
||||
4. vignette (free)
|
||||
```
|
||||
|
||||
### Thai Text Hero (~$0.003)
|
||||
```
|
||||
1. Ensure patch applied
|
||||
2. generate flux-schnell ($0.003) → dark background
|
||||
3. text --font "Kanit" (free) → Thai title
|
||||
4. grade cinematic (free)
|
||||
```
|
||||
|
||||
### Social Post (~$0.003)
|
||||
```
|
||||
1. generate flux-schnell ($0.003) → abstract background
|
||||
2. grade vibrant (free)
|
||||
```
|
||||
|
||||
## Important Rules
|
||||
|
||||
1. **Always load credentials before FAL commands:** `set -a && source ~/.config/opencode/.env && set +a`
|
||||
2. **Plan before generating** — ask about cost if workflow is complex
|
||||
3. **Re-patch after updates** — Thai fonts break after `bun install -g picture-it`
|
||||
4. **Use --model to override** — default models may not be cheapest for the task
|
||||
5. **Use free operations first** — crop, grade, vignette are free
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Problem | Solution |
|
||||
|---|---|
|
||||
| "command not found" | Add bun to PATH: `export PATH="~/snap/bun-js/87/.bun/bin:$PATH"` |
|
||||
| Thai shows ▢ | Run: `bun ~/.hermes/skills/website-creator/creative/picture-it/scripts/thai-font-patch.ts --force` |
|
||||
| "No FAL API key" | Load credentials: `set -a && source ~/.config/opencode/.env && set +a` |
|
||||
| Fonts not found | Run: `picture-it download-fonts` |
|
||||
|
||||
## Related Skills
|
||||
|
||||
- [[banner-design]] — Use picture-it to generate banner images
|
||||
- [[website-creator]] — Integrate picture-it outputs into websites
|
||||
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"blog-hero": {
|
||||
"description": "Dark cinematic blog hero with logo overlay",
|
||||
"estimated_cost": "$0.043",
|
||||
"pipeline": [
|
||||
{ "op": "generate", "prompt": "<scene_description>", "size": "1200x630" },
|
||||
{ "op": "edit", "prompt": "place Figure 1 as a large glowing element in center", "assets": ["logo.png"] },
|
||||
{ "op": "grade", "name": "cinematic" },
|
||||
{ "op": "vignette" }
|
||||
]
|
||||
},
|
||||
"product-comparison": {
|
||||
"description": "Side-by-side product comparison with remove-bg",
|
||||
"estimated_cost": "$0.01",
|
||||
"pipeline": [
|
||||
{ "op": "generate", "prompt": "clean gradient backdrop, professional lighting", "size": "1200x630" },
|
||||
{ "op": "remove-bg", "assets": ["product-a.png"] },
|
||||
{ "op": "compose", "overlays": "comparison-layout.json" },
|
||||
{ "op": "grade", "name": "clean" }
|
||||
]
|
||||
},
|
||||
"youtube-thumbnail": {
|
||||
"description": "Bold text-behind-subject YouTube thumbnail",
|
||||
"estimated_cost": "$0.07",
|
||||
"pipeline": [
|
||||
{ "op": "generate", "prompt": "<scene>", "size": "1280x720" },
|
||||
{ "op": "edit", "prompt": "place text '<title>' behind the subject, keep subject fully visible", "assets": [] },
|
||||
{ "op": "grade", "name": "cinematic" },
|
||||
{ "op": "vignette" }
|
||||
]
|
||||
},
|
||||
"instagram-square": {
|
||||
"description": "Clean Instagram square post",
|
||||
"estimated_cost": "$0.003-0.15",
|
||||
"pipeline": [
|
||||
{ "op": "generate", "prompt": "<prompt>", "size": "1080x1080" },
|
||||
{ "op": "grade", "name": "vibrant" }
|
||||
]
|
||||
},
|
||||
"social-card": {
|
||||
"description": "Text overlay on generated background",
|
||||
"estimated_cost": "$0.003",
|
||||
"pipeline": [
|
||||
{ "op": "generate", "prompt": "<background_prompt>", "size": "1200x630" },
|
||||
{ "op": "text", "title": "<title>", "fontSize": 64, "font": "Space Grotesk" },
|
||||
{ "op": "grade", "name": "cinematic" }
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,268 @@
|
||||
#!/usr/bin/env bun
|
||||
/**
|
||||
* Thai Font Patcher for picture-it
|
||||
*
|
||||
* Detects if Thai font entries are missing from picture-it's FONT_FILES array
|
||||
* in dist/index.js, and patches them if needed.
|
||||
*
|
||||
* This is idempotent — safe to run multiple times.
|
||||
* Run this after every `picture-it` update.
|
||||
*
|
||||
* Usage:
|
||||
* bun thai-font-patch.ts # patch if needed
|
||||
* bun thai-font-patch.ts --check # just check, don't patch
|
||||
* bun thai-font-patch.ts --force # re-patch even if already patched
|
||||
*/
|
||||
|
||||
import { readFileSync, writeFileSync, existsSync, cpSync, mkdirSync } from "fs";
|
||||
import { join, dirname } from "path";
|
||||
import { execSync } from "child_process";
|
||||
|
||||
const THAI_FONT_FILES_SIGNATURE = "thai/NotoSansThai-Regular.ttf";
|
||||
|
||||
const THAI_FONTS_ENTRY = ` },
|
||||
// Thai fonts
|
||||
{
|
||||
name: "Noto Sans Thai",
|
||||
file: "thai/NotoSansThai-Regular.ttf",
|
||||
weight: 400,
|
||||
style: "normal",
|
||||
url: "https://fonts.gstatic.com/s/notosansthai/v29/iJWnBXeUZi_OHPqn4wq6hQ2_hbJ1xyN9wd43SofNWcd1MKVQt_So_9CdU5RtpzE.ttf"
|
||||
},
|
||||
{
|
||||
name: "Noto Sans Thai",
|
||||
file: "thai/NotoSansThai-Bold.ttf",
|
||||
weight: 700,
|
||||
style: "normal",
|
||||
url: "https://fonts.gstatic.com/s/notosansthai/v29/iJWnBXeUZi_OHPqn4wq6hQ2_hbJ1xyN9wd43SofNWcd1MKVQt_So_9CdU3NqpzE.ttf"
|
||||
},
|
||||
{
|
||||
name: "Kanit",
|
||||
file: "thai/Kanit-Regular.ttf",
|
||||
weight: 400,
|
||||
style: "normal",
|
||||
url: "https://fonts.gstatic.com/s/kanit/v17/nKKZ-Go6G5tXcoaS.ttf"
|
||||
},
|
||||
{
|
||||
name: "Kanit",
|
||||
file: "thai/Kanit-Bold.ttf",
|
||||
weight: 700,
|
||||
style: "normal",
|
||||
url: "https://fonts.gstatic.com/s/kanit/v17/nKKU-Go6G5tXcr4uPiWg.ttf"
|
||||
}
|
||||
];`
|
||||
|
||||
// Thai font download URLs
|
||||
const THAI_FONTS_TO_DOWNLOAD = [
|
||||
{
|
||||
name: "NotoSansThai-Regular.ttf",
|
||||
url: "https://fonts.gstatic.com/s/notosansthai/v29/iJWnBXeUZi_OHPqn4wq6hQ2_hbJ1xyN9wd43SofNWcd1MKVQt_So_9CdU5RtpzE.ttf",
|
||||
},
|
||||
{
|
||||
name: "NotoSansThai-Bold.ttf",
|
||||
url: "https://fonts.gstatic.com/s/notosansthai/v29/iJWnBXeUZi_OHPqn4wq6hQ2_hbJ1xyN9wd43SofNWcd1MKVQt_So_9CdU3NqpzE.ttf",
|
||||
},
|
||||
{
|
||||
name: "Kanit-Regular.ttf",
|
||||
url: "https://fonts.gstatic.com/s/kanit/v17/nKKZ-Go6G5tXcoaS.ttf",
|
||||
},
|
||||
{
|
||||
name: "Kanit-Bold.ttf",
|
||||
url: "https://fonts.gstatic.com/s/kanit/v17/nKKU-Go6G5tXcr4uPiWg.ttf",
|
||||
},
|
||||
];
|
||||
|
||||
function findPictureItDist(): string | null {
|
||||
const home = process.env.HOME || "";
|
||||
const searchPaths = [
|
||||
// bun-js snap (primary)
|
||||
join(home, "snap/bun-js/87/.bun/install/global/node_modules/picture-it/dist/index.js"),
|
||||
// npm global
|
||||
join(home, ".npm-global/lib/node_modules/picture-it/dist/index.js"),
|
||||
];
|
||||
|
||||
for (const p of searchPaths) {
|
||||
if (existsSync(p)) return p;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function getPictureItVersion(installDir: string): string {
|
||||
try {
|
||||
const pkgPath = join(installDir, "package.json");
|
||||
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
||||
return pkg.version || "unknown";
|
||||
} catch {
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
function getInstallDir(distPath: string): string {
|
||||
return dirname(dirname(distPath));
|
||||
}
|
||||
|
||||
function isAlreadyPatched(distPath: string): boolean {
|
||||
const content = readFileSync(distPath, "utf-8");
|
||||
return content.includes(THAI_FONT_FILES_SIGNATURE);
|
||||
}
|
||||
|
||||
async function downloadThaiFonts(fontDir: string): Promise<string[]> {
|
||||
mkdirSync(fontDir, { recursive: true });
|
||||
const downloaded: string[] = [];
|
||||
for (const font of THAI_FONTS_TO_DOWNLOAD) {
|
||||
const outPath = join(fontDir, font.name);
|
||||
if (existsSync(outPath)) {
|
||||
const stat = readFileSync(outPath);
|
||||
if (stat.length > 1000) {
|
||||
console.log(` Already exists: ${font.name}`);
|
||||
downloaded.push(font.name);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
console.log(` Downloading: ${font.name}`);
|
||||
const res = await fetch(font.url);
|
||||
if (!res.ok) throw new Error(`Failed to download ${font.name}: ${res.status}`);
|
||||
const buf = await res.arrayBuffer();
|
||||
writeFileSync(outPath, Buffer.from(buf));
|
||||
downloaded.push(font.name);
|
||||
}
|
||||
return downloaded;
|
||||
}
|
||||
|
||||
function patchDist(distPath: string): boolean {
|
||||
const content = readFileSync(distPath, "utf-8");
|
||||
|
||||
// Find DM Serif Display closing brace in FONT_FILES array
|
||||
// Pattern: the last entry in FONT_FILES ends with:
|
||||
// url: "https://fonts.gstatic.com/s/dmserifdisplay/v17/..."
|
||||
// }
|
||||
// ];
|
||||
// We need to replace the closing `}` with Thai fonts entries + ];
|
||||
|
||||
const dmSerifEntryEnd = `name: "DM Serif Display",
|
||||
file: "DMSerifDisplay-Regular.ttf",
|
||||
weight: 400,
|
||||
style: "normal",
|
||||
url: "https://fonts.gstatic.com/s/dmserifdisplay/v17/-nFnOHM81r4j6k0gjAW3mujVU2B2K_c.ttf"
|
||||
}`;
|
||||
|
||||
// Find the closing of the array after DM Serif Display
|
||||
// We look for the pattern: DM Serif Display entry ... }; var cachedFonts
|
||||
// The `];` after the closing `}` of DM Serif Display is the end of FONT_FILES
|
||||
const fontFilesEndPattern = /(\n\s+url: "https:\/\/fonts\.gstatic\.com\/s\/dmserifdisplay\/v17\/[^"]+"\n\s+}\n)\];/;
|
||||
|
||||
const match = content.match(fontFilesEndPattern);
|
||||
if (!match) {
|
||||
// Try alternate pattern (might differ in minified versions)
|
||||
const altPattern = /(\}\n)\];/;
|
||||
const altMatch = content.match(altPattern);
|
||||
if (!altMatch) {
|
||||
throw new Error("Could not find end of FONT_FILES array. picture-it format may have changed.");
|
||||
}
|
||||
// Just replace the first occurrence of ]; after a }
|
||||
const newContent = content.replace(altPattern, THAI_FONTS_ENTRY.replace(" },", match?.[1] || " }").endsWith("}") ? THAI_FONTS_ENTRY : ` },${THAI_FONTS_ENTRY.split(" },")[1]}`);
|
||||
writeFileSync(distPath, newContent);
|
||||
return true;
|
||||
}
|
||||
|
||||
const patched = content.replace(
|
||||
fontFilesEndPattern,
|
||||
THAI_FONTS_ENTRY
|
||||
);
|
||||
|
||||
if (patched === content) {
|
||||
return false;
|
||||
}
|
||||
|
||||
writeFileSync(distPath, patched);
|
||||
return true;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const args = process.argv.slice(2);
|
||||
const checkOnly = args.includes("--check");
|
||||
const force = args.includes("--force");
|
||||
|
||||
console.log("[Thai Font Patcher for picture-it]");
|
||||
console.log("");
|
||||
|
||||
const distPath = findPictureItDist();
|
||||
|
||||
if (!distPath) {
|
||||
console.log("ERROR: picture-it not found.");
|
||||
console.log("Is picture-it installed? Run: bun install -g picture-it");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const installDir = getInstallDir(distPath);
|
||||
const version = getPictureItVersion(installDir);
|
||||
|
||||
console.log(`picture-it found: v${version}`);
|
||||
console.log(`Install dir: ${installDir}`);
|
||||
console.log(`dist path: ${distPath}`);
|
||||
console.log("");
|
||||
|
||||
const alreadyPatched = isAlreadyPatched(distPath);
|
||||
|
||||
if (alreadyPatched && !force) {
|
||||
console.log("Thai fonts: ALREADY PATCHED ✓");
|
||||
console.log("No action needed.");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (checkOnly) {
|
||||
console.log("Thai fonts: NOT PATCHED ✗");
|
||||
console.log("Run without --check to apply patch.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (force && alreadyPatched) {
|
||||
console.log("Force mode: re-patching...");
|
||||
}
|
||||
|
||||
// Step 1: Download Thai fonts
|
||||
console.log("Step 1: Ensuring Thai fonts are downloaded...");
|
||||
const fontDir = join(process.env.HOME || "", ".picture-it/fonts/thai");
|
||||
try {
|
||||
await downloadThaiFonts(fontDir);
|
||||
} catch (e: any) {
|
||||
console.error(` Failed to download fonts: ${e.message}`);
|
||||
console.error("You can download them manually or try again.");
|
||||
}
|
||||
console.log("");
|
||||
|
||||
// Step 2: Patch dist/index.js
|
||||
console.log("Step 2: Patching dist/index.js...");
|
||||
try {
|
||||
const success = patchDist(distPath);
|
||||
if (success) {
|
||||
console.log(" Patched dist/index.js ✓");
|
||||
} else {
|
||||
console.log(" No changes needed (or patch failed silently)");
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.error(` Patch failed: ${e.message}`);
|
||||
console.error("Please report this issue.");
|
||||
process.exit(1);
|
||||
}
|
||||
console.log("");
|
||||
|
||||
// Verify
|
||||
const verifyPatched = isAlreadyPatched(distPath);
|
||||
if (verifyPatched) {
|
||||
console.log("RESULT: Thai font patch applied successfully ✓");
|
||||
console.log("");
|
||||
console.log("You can now use Thai fonts:");
|
||||
console.log(' picture-it text -i input.png --title "ทดสอบ" --font "Noto Sans Thai" -o out.png');
|
||||
console.log(' picture-it text -i input.png --title "ทดสอบ" --font "Kanit" -o out.png');
|
||||
} else {
|
||||
console.error("RESULT: Patch verification failed ✗");
|
||||
console.error("The patch may not have been applied correctly.");
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((e: Error) => {
|
||||
console.error(`Error: ${e.message}`);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user