#!/usr/bin/env bun import { existsSync, mkdirSync, appendFileSync, writeFileSync } from "fs"; import { join } from "path"; import { randomUUID } from "crypto"; // Constants const SKILL_NAME = "video-thumbnail"; const SESSION_ID = randomUUID().slice(0, 8); const SESSION_TIMESTAMP = new Date().toISOString().replace(/[:.]/g, "_").slice(0, 19); // Environment const SKILLS_OUTPUT_DIR = process.env.SKILLS_OUTPUT_DIR || join(process.cwd(), ".skills"); const EXPORTS_DIR = join(SKILLS_OUTPUT_DIR, "exports", SKILL_NAME, SESSION_ID); const LOGS_DIR = join(SKILLS_OUTPUT_DIR, "logs", SKILL_NAME); const OPENAI_API_KEY = process.env.OPENAI_API_KEY; const OPENAI_IMAGE_URL = "https://api.openai.com/v1/images/generations"; // Types interface ThumbnailResult { variant: string; style: string; filename: string; prompt: string; elements: string[]; } interface Options { title: string; style: string; variants: number; text: string; primaryColor: string; textColor: string; platform: string; face: boolean; emotion: string; noText: boolean; format: string; verbose: boolean; } // Thumbnail styles const STYLES: Record = { reaction: { description: "expressive face with shocked or surprised expression, bold text overlay, bright saturated colors", elements: ["expressive face", "bold text", "bright colors", "emotion-driven"], }, "before-after": { description: "split screen comparison showing transformation, clear before and after states, arrow or transition element", elements: ["split screen", "comparison", "transformation arrow", "contrast"], }, listicle: { description: "large bold number as focal point, supporting visual context, clean typography", elements: ["big number", "list preview", "organized layout"], }, mystery: { description: "intriguing visual with hidden or blurred element, question mark or reveal teaser, curiosity-inducing", elements: ["mystery element", "blur/hidden", "question mark", "curiosity hook"], }, tutorial: { description: "screenshot or preview of end result, step indicator or progress element, educational feel", elements: ["result preview", "step numbers", "how-to feel"], }, minimal: { description: "clean simple design with lots of negative space, professional typography, brand-focused", elements: ["clean design", "negative space", "professional"], }, }; // Emotions for face thumbnails const EMOTIONS: Record = { surprised: "eyes wide open, mouth slightly open, raised eyebrows, genuine surprise expression", shocked: "extremely surprised expression, jaw dropped, wide eyes, dramatic reaction", happy: "big genuine smile, warm friendly expression, approachable", excited: "energetic expression, big smile, enthusiastic body language", curious: "thoughtful expression, slightly raised eyebrow, engaged look", serious: "professional confident expression, direct eye contact, authoritative", }; // Utilities function ensureDir(dir: string): void { if (!existsSync(dir)) mkdirSync(dir, { recursive: true }); } function log(message: string, level: "info" | "error" | "success" = "info"): void { ensureDir(LOGS_DIR); appendFileSync(join(LOGS_DIR, `log_${SESSION_TIMESTAMP}_${SESSION_ID}.log`), `[${new Date().toISOString()}] [${level.toUpperCase()}] ${message}\n`); console.log(`[${level.toUpperCase()}] ${message}`); } function parseArguments(): Options { const args = process.argv.slice(2); const options: Options = { title: "", style: "auto", variants: 2, text: "", primaryColor: "auto", textColor: "white", platform: "youtube", face: true, emotion: "surprised", noText: false, format: "png", verbose: false, }; for (let i = 0; i < args.length; i++) { const arg = args[i]; switch (arg) { case "--style": options.style = args[++i]; break; case "--variants": options.variants = parseInt(args[++i], 10); break; case "--text": options.text = args[++i]; break; case "--primary-color": options.primaryColor = args[++i]; break; case "--text-color": options.textColor = args[++i]; break; case "--platform": options.platform = args[++i]; break; case "--face": options.face = true; break; case "--no-face": options.face = false; break; case "--emotion": options.emotion = args[++i]; break; case "--no-text": options.noText = true; break; case "--format": options.format = args[++i]; break; case "--verbose": options.verbose = true; break; case "--help": case "-h": printHelp(); process.exit(0); default: if (!arg.startsWith("-") && !options.title) options.title = arg; } } // Auto-detect style from title if (options.style === "auto") { const titleLower = options.title.toLowerCase(); if (/^\d+\s/.test(options.title) || titleLower.includes("top ") || titleLower.includes("best ")) { options.style = "listicle"; } else if (titleLower.includes("before") || titleLower.includes("after") || titleLower.includes("transform")) { options.style = "before-after"; } else if (titleLower.includes("how to") || titleLower.includes("tutorial") || titleLower.includes("learn")) { options.style = "tutorial"; } else if (titleLower.includes("?") || titleLower.includes("secret") || titleLower.includes("reveal")) { options.style = "mystery"; } else { options.style = "reaction"; } } return options; } function printHelp(): void { console.log(` Video Thumbnail Generator - Create click-worthy YouTube thumbnails Usage: skills run video-thumbnail -- "