Vendor OpenClaw source as Adolf fork baseline
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Adolf is a fork/vendored clone of github.com/openclaw/openclaw (v2026.6.11), free to diverge. Tree copied sans upstream .git; upstream remote added for future syncs. Node pinned to 24 (.nvmrc); engines already require >=22.19. Preserves docs/ARCHITECTURE.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LeqyaxJF2nbRXJtae2kNB2
This commit is contained in:
398
skills/meme-maker/scripts/meme.mjs
Executable file
398
skills/meme-maker/scripts/meme.mjs
Executable file
@@ -0,0 +1,398 @@
|
||||
#!/usr/bin/env node
|
||||
import { Buffer } from "node:buffer";
|
||||
import { existsSync } from "node:fs";
|
||||
import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
|
||||
import { homedir } from "node:os";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const BASE_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const TEMPLATES_PATH = path.join(BASE_DIR, "references", "templates.json");
|
||||
const IMGFLIP_GET_MEMES_URL = "https://api.imgflip.com/get_memes";
|
||||
const IMGFLIP_CAPTION_URL = "https://api.imgflip.com/caption_image";
|
||||
const USER_AGENT = "OpenClawMemeMaker/1.0";
|
||||
const STOPWORDS = new Set([
|
||||
"a",
|
||||
"an",
|
||||
"and",
|
||||
"are",
|
||||
"for",
|
||||
"in",
|
||||
"is",
|
||||
"it",
|
||||
"of",
|
||||
"on",
|
||||
"or",
|
||||
"the",
|
||||
"this",
|
||||
"to",
|
||||
"use",
|
||||
"with",
|
||||
]);
|
||||
|
||||
function usage(exitCode = 0) {
|
||||
const out = exitCode === 0 ? console.log : console.error;
|
||||
out(`Usage:
|
||||
meme.mjs list [--json]
|
||||
meme.mjs search <query> [--json]
|
||||
meme.mjs suggest <topic> [--limit N] [--json]
|
||||
meme.mjs render <template> --text TEXT ... [--out PATH] [--service local|imgflip]
|
||||
meme.mjs refresh [--limit N] [--json]`);
|
||||
process.exit(exitCode);
|
||||
}
|
||||
|
||||
function parseArgs(argv) {
|
||||
const flags = { _: [] };
|
||||
for (let i = 0; i < argv.length; i += 1) {
|
||||
const arg = argv[i];
|
||||
if (!arg.startsWith("--")) {
|
||||
flags._.push(arg);
|
||||
continue;
|
||||
}
|
||||
const eq = arg.indexOf("=");
|
||||
const key = eq === -1 ? arg.slice(2) : arg.slice(2, eq);
|
||||
const inline = eq === -1 ? undefined : arg.slice(eq + 1);
|
||||
if (["json", "help"].includes(key)) {
|
||||
flags[key] = true;
|
||||
continue;
|
||||
}
|
||||
const value = inline ?? argv[i + 1];
|
||||
if (inline === undefined) i += 1;
|
||||
if (value === undefined) throw new Error(`Missing value for --${key}`);
|
||||
if (key === "text") {
|
||||
flags.text = [...(flags.text ?? []), value];
|
||||
} else {
|
||||
flags[key] = value;
|
||||
}
|
||||
}
|
||||
return flags;
|
||||
}
|
||||
|
||||
function normalize(value) {
|
||||
return String(value)
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, " ")
|
||||
.trim();
|
||||
}
|
||||
|
||||
function tokens(value) {
|
||||
return normalize(value)
|
||||
.split(/\s+/)
|
||||
.filter((token) => token && !STOPWORDS.has(token));
|
||||
}
|
||||
|
||||
async function loadTemplates() {
|
||||
return JSON.parse(await readFile(TEMPLATES_PATH, "utf8"));
|
||||
}
|
||||
|
||||
function templateHaystack(template) {
|
||||
return [
|
||||
template.id,
|
||||
template.name,
|
||||
template.use,
|
||||
...(template.aliases ?? []),
|
||||
...(template.tags ?? []),
|
||||
...(template.fields ?? []),
|
||||
].join(" ");
|
||||
}
|
||||
|
||||
function scoreTemplate(template, query) {
|
||||
const queryTokens = tokens(query);
|
||||
const haystack = normalize(templateHaystack(template));
|
||||
let score = 0;
|
||||
for (const token of queryTokens) {
|
||||
if (normalize(template.id).includes(token)) score += 8;
|
||||
if (normalize(template.name).includes(token)) score += 6;
|
||||
if ((template.aliases ?? []).some((alias) => normalize(alias).includes(token))) score += 5;
|
||||
if ((template.tags ?? []).some((tag) => normalize(tag).includes(token))) score += 4;
|
||||
if (normalize(template.use).includes(token)) score += 3;
|
||||
if (haystack.includes(token)) score += 1;
|
||||
}
|
||||
return score;
|
||||
}
|
||||
|
||||
function findTemplate(templates, selector) {
|
||||
const wanted = normalize(selector);
|
||||
const exact = templates.find(
|
||||
(template) =>
|
||||
normalize(template.id) === wanted ||
|
||||
normalize(template.name) === wanted ||
|
||||
(template.aliases ?? []).some((alias) => normalize(alias) === wanted),
|
||||
);
|
||||
if (exact) return exact;
|
||||
const ranked = templates
|
||||
.map((template) => ({ template, score: scoreTemplate(template, selector) }))
|
||||
.filter((entry) => entry.score > 0)
|
||||
.sort((a, b) => b.score - a.score);
|
||||
return ranked[0]?.template;
|
||||
}
|
||||
|
||||
function printTemplates(templates, json) {
|
||||
if (json) {
|
||||
console.log(JSON.stringify(templates, null, 2));
|
||||
return;
|
||||
}
|
||||
for (const template of templates) {
|
||||
console.log(`${template.id}: ${template.name}`);
|
||||
console.log(` use: ${template.use}`);
|
||||
console.log(` fields: ${template.fields.join(", ")}`);
|
||||
console.log(` kym: ${template.kymUrl}`);
|
||||
}
|
||||
}
|
||||
|
||||
function cacheRoot() {
|
||||
const root =
|
||||
process.env.XDG_CACHE_HOME ||
|
||||
(process.platform === "darwin"
|
||||
? path.join(homedir(), "Library", "Caches")
|
||||
: path.join(homedir(), ".cache"));
|
||||
return path.join(root, "openclaw", "meme-maker");
|
||||
}
|
||||
|
||||
function extFromUrl(url) {
|
||||
const ext = path.extname(new URL(url).pathname).toLowerCase();
|
||||
return ext && ext.length <= 5 ? ext : ".img";
|
||||
}
|
||||
|
||||
async function fetchBuffer(url) {
|
||||
const response = await fetch(url, { headers: { "User-Agent": USER_AGENT } });
|
||||
if (!response.ok) throw new Error(`Fetch failed ${response.status} for ${url}`);
|
||||
return Buffer.from(await response.arrayBuffer());
|
||||
}
|
||||
|
||||
async function cachedTemplateImage(template) {
|
||||
const dir = cacheRoot();
|
||||
await mkdir(dir, { recursive: true });
|
||||
const file = path.join(
|
||||
dir,
|
||||
`${template.id}-${template.imgflipId}${extFromUrl(template.imageUrl)}`,
|
||||
);
|
||||
if (existsSync(file)) return { file, buffer: await readFile(file) };
|
||||
const buffer = await fetchBuffer(template.imageUrl);
|
||||
await writeFile(file, buffer);
|
||||
return { file, buffer };
|
||||
}
|
||||
|
||||
function escapeXml(value) {
|
||||
return String(value)
|
||||
.replaceAll("&", "&")
|
||||
.replaceAll("<", "<")
|
||||
.replaceAll(">", ">")
|
||||
.replaceAll('"', """);
|
||||
}
|
||||
|
||||
function wrapText(text, maxChars) {
|
||||
const words = String(text).trim().split(/\s+/).filter(Boolean);
|
||||
const lines = [];
|
||||
let current = "";
|
||||
for (const word of words) {
|
||||
const candidate = current ? `${current} ${word}` : word;
|
||||
if (candidate.length <= maxChars || !current) {
|
||||
current = candidate;
|
||||
} else {
|
||||
lines.push(current);
|
||||
current = word;
|
||||
}
|
||||
}
|
||||
if (current) lines.push(current);
|
||||
return lines.length ? lines : [""];
|
||||
}
|
||||
|
||||
function textSvg(text, box, width, height, index) {
|
||||
const x = box.x * width;
|
||||
const y = box.y * height;
|
||||
const w = box.w * width;
|
||||
const h = box.h * height;
|
||||
const centerX = x + w / 2;
|
||||
const centerY = y + h / 2;
|
||||
const maxChars = Math.max(8, Math.floor(w / Math.max(12, width * 0.032)));
|
||||
let lines = wrapText(text, maxChars).slice(0, 5);
|
||||
const fontSize = Math.max(
|
||||
18,
|
||||
Math.min(
|
||||
h / (lines.length * 1.15),
|
||||
(w / Math.max(...lines.map((line) => line.length), 1)) * 1.65,
|
||||
width * 0.07,
|
||||
),
|
||||
);
|
||||
const lineHeight = fontSize * 1.08;
|
||||
const totalHeight = lineHeight * lines.length;
|
||||
const rotate = box.rotate ? ` rotate(${box.rotate} ${centerX} ${centerY})` : "";
|
||||
lines = lines.map(escapeXml);
|
||||
const tspans = lines
|
||||
.map((line, lineIndex) => {
|
||||
const dy = lineIndex === 0 ? -(totalHeight - lineHeight) / 2 : lineHeight;
|
||||
return `<tspan x="${centerX.toFixed(1)}" dy="${dy.toFixed(1)}">${line}</tspan>`;
|
||||
})
|
||||
.join("");
|
||||
return `<text class="meme-text" data-box="${index}" transform="translate(0 ${centerY.toFixed(1)})${rotate}" font-size="${fontSize.toFixed(1)}">${tspans}</text>`;
|
||||
}
|
||||
|
||||
function defaultBoxes(count) {
|
||||
if (count <= 1) return [{ x: 0.06, y: 0.74, w: 0.88, h: 0.18 }];
|
||||
if (count === 2) {
|
||||
return [
|
||||
{ x: 0.06, y: 0.05, w: 0.88, h: 0.18 },
|
||||
{ x: 0.06, y: 0.76, w: 0.88, h: 0.18 },
|
||||
];
|
||||
}
|
||||
return Array.from({ length: count }, (_, index) => ({
|
||||
x: 0.05,
|
||||
y: 0.04 + index * (0.9 / count),
|
||||
w: 0.9,
|
||||
h: Math.min(0.16, 0.8 / count),
|
||||
}));
|
||||
}
|
||||
|
||||
async function renderLocal(template, texts, flags) {
|
||||
const { buffer } = await cachedTemplateImage(template);
|
||||
const imageMime = extFromUrl(template.imageUrl) === ".png" ? "image/png" : "image/jpeg";
|
||||
const imageData = `data:${imageMime};base64,${buffer.toString("base64")}`;
|
||||
const boxes = template.boxes?.length
|
||||
? template.boxes
|
||||
: defaultBoxes(texts.length || template.fields.length);
|
||||
const width = Number(template.width);
|
||||
const height = Number(template.height);
|
||||
const textNodes = texts
|
||||
.map((text, index) =>
|
||||
textSvg(text, boxes[index] ?? boxes[boxes.length - 1], width, height, index),
|
||||
)
|
||||
.join("\n");
|
||||
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">
|
||||
<style>
|
||||
.meme-text {
|
||||
font-family: Impact, "Arial Black", Arial, sans-serif;
|
||||
font-weight: 900;
|
||||
fill: #fff;
|
||||
stroke: #000;
|
||||
stroke-width: ${Math.max(3, Math.round(width / 250))};
|
||||
paint-order: stroke;
|
||||
text-anchor: middle;
|
||||
dominant-baseline: middle;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
</style>
|
||||
<image href="${imageData}" x="0" y="0" width="${width}" height="${height}" preserveAspectRatio="none"/>
|
||||
${textNodes}
|
||||
</svg>
|
||||
`;
|
||||
const out = flags.out ?? path.resolve(process.cwd(), `${template.id}.svg`);
|
||||
await mkdir(path.dirname(path.resolve(out)), { recursive: true });
|
||||
if (path.extname(out).toLowerCase() === ".png") {
|
||||
let sharp;
|
||||
try {
|
||||
sharp = (await import("sharp")).default;
|
||||
} catch {
|
||||
throw new Error(
|
||||
"PNG output needs the optional sharp package. Use --out meme.svg or install sharp near the skill runner.",
|
||||
);
|
||||
}
|
||||
await sharp(Buffer.from(svg)).png().toFile(out);
|
||||
} else {
|
||||
await writeFile(out, svg, "utf8");
|
||||
}
|
||||
const size = (await stat(out)).size;
|
||||
console.log(`${out} (${size} bytes)`);
|
||||
}
|
||||
|
||||
async function renderImgflip(template, texts, flags) {
|
||||
const username = flags.username || process.env.IMGFLIP_USER;
|
||||
const password = flags.password || process.env.IMGFLIP_PASS;
|
||||
if (!username || !password) {
|
||||
throw new Error(
|
||||
"Imgflip service requires IMGFLIP_USER and IMGFLIP_PASS, or --username/--password.",
|
||||
);
|
||||
}
|
||||
const body = new URLSearchParams({
|
||||
template_id: template.imgflipId,
|
||||
username,
|
||||
password,
|
||||
});
|
||||
if ((template.boxes?.length ?? template.fields.length) <= 2) {
|
||||
texts.forEach((text, index) => body.set(`text${index}`, text));
|
||||
} else {
|
||||
texts.forEach((text, index) => body.set(`boxes[${index}][text]`, text));
|
||||
}
|
||||
const response = await fetch(IMGFLIP_CAPTION_URL, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
"User-Agent": USER_AGENT,
|
||||
},
|
||||
body,
|
||||
});
|
||||
const payload = await response.json();
|
||||
if (!payload.success) throw new Error(payload.error_message || "Imgflip caption_image failed");
|
||||
console.log(payload.data.url);
|
||||
}
|
||||
|
||||
async function refresh(flags) {
|
||||
const limit = Number(flags.limit || 25);
|
||||
const response = await fetch(IMGFLIP_GET_MEMES_URL, { headers: { "User-Agent": USER_AGENT } });
|
||||
if (!response.ok) throw new Error(`Imgflip get_memes failed ${response.status}`);
|
||||
const payload = await response.json();
|
||||
const memes = payload.data.memes.slice(0, limit);
|
||||
if (flags.json) {
|
||||
console.log(JSON.stringify(memes, null, 2));
|
||||
} else {
|
||||
for (const meme of memes) {
|
||||
console.log(
|
||||
`${meme.id}: ${meme.name} (${meme.width}x${meme.height}, boxes=${meme.box_count})`,
|
||||
);
|
||||
console.log(` ${meme.url}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const flags = parseArgs(process.argv.slice(2));
|
||||
if (flags.help) usage(0);
|
||||
const [command, ...rest] = flags._;
|
||||
if (!command) usage(1);
|
||||
const templates = await loadTemplates();
|
||||
|
||||
if (command === "list") {
|
||||
printTemplates(templates, flags.json);
|
||||
return;
|
||||
}
|
||||
if (command === "search" || command === "suggest") {
|
||||
const query = rest.join(" ");
|
||||
if (!query) throw new Error(`${command} needs a query`);
|
||||
const limit = Number(flags.limit || (command === "suggest" ? 5 : 20));
|
||||
const ranked = templates
|
||||
.map((template) => ({ ...template, score: scoreTemplate(template, query) }))
|
||||
.filter((template) => template.score > 0)
|
||||
.sort((a, b) => b.score - a.score)
|
||||
.slice(0, limit);
|
||||
printTemplates(ranked, flags.json);
|
||||
return;
|
||||
}
|
||||
if (command === "render") {
|
||||
const selector = rest.join(" ");
|
||||
if (!selector) throw new Error("render needs a template id/name");
|
||||
const template = findTemplate(templates, selector);
|
||||
if (!template) throw new Error(`No matching template: ${selector}`);
|
||||
const texts = flags.text ?? [];
|
||||
if (!texts.length)
|
||||
throw new Error(`Add text with --text. Fields: ${template.fields.join(", ")}`);
|
||||
const service = flags.service || "local";
|
||||
if (service === "local") {
|
||||
await renderLocal(template, texts, flags);
|
||||
} else if (service === "imgflip") {
|
||||
await renderImgflip(template, texts, flags);
|
||||
} else {
|
||||
throw new Error(`Unknown service: ${service}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (command === "refresh") {
|
||||
await refresh(flags);
|
||||
return;
|
||||
}
|
||||
usage(1);
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(`error: ${error.message}`);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user