Files
bzuccaro 771cb81eb3 Prepare Elgato Marketplace listing: brand author, store assets, listing text
- manifest Author -> c4ch3c4d3 and repackage .streamDeckPlugin
- scripts/store-assets.mts renders real key SVGs into store PNGs (app icon 288, thumbnail + 3 gallery 1920x960)
- store-assets/listing.md: description, tags, release notes, submission checklist
2026-08-14 13:25:38 -06:00

189 lines
7.9 KiB
TypeScript

import { mkdirSync, rmSync, writeFileSync } from "node:fs";
import { execFileSync } from "node:child_process";
import { fileURLToPath } from "node:url";
import path from "node:path";
import { renderGpuGraph, renderInflight, renderUsage } from "../src/lib/render";
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const OUT = path.join(ROOT, "store-assets");
const TMP = path.join(OUT, ".gen");
const FONT = "Helvetica, Arial, sans-serif";
const BG = "#0e1117";
const PANEL = "#161b26";
const STROKE = "#242b38";
const MUTED = "#8b93a5";
const WHITE = "#e6e9ef";
const GREEN = "#3fae5a";
const RED = "#e0453a";
const AMBER = "#d9a02a";
rmSync(TMP, { recursive: true, force: true });
mkdirSync(TMP, { recursive: true });
function convert(src: string, dst: string, w: number, h: number): void {
execFileSync("rsvg-convert", ["--unlimited", "-w", String(w), "-h", String(h), "-o", dst, src]);
}
function rasterizeKey(name: string, svg: string, scale = 3): string {
const svgPath = path.join(TMP, `${name}.svg`);
const pngPath = path.join(TMP, `${name}.png`);
writeFileSync(svgPath, svg);
convert(svgPath, pngPath, 72 * scale, 72 * scale);
return `${name}.png`;
}
function svg(w: number, h: number, body: string): string {
return `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}">
<rect width="${w}" height="${h}" fill="${BG}"/>
${body}
</svg>`;
}
function text(content: string, x: number, y: number, size: number, fill: string, weight = "normal", anchor = "middle"): string {
return ` <text x="${x}" y="${y}" text-anchor="${anchor}" font-family="${FONT}" font-size="${size}" font-weight="${weight}" fill="${fill}">${content}</text>`;
}
function keyImage(png: string, cx: number, cy: number, size: number): string {
return ` <image href="${png}" x="${cx - size / 2}" y="${cy - size / 2}" width="${size}" height="${size}"/>`;
}
function keyWithLabel(png: string, label: string, cx: number, cy: number, size: number): string {
return `${keyImage(png, cx, cy, size)}
<rect x="${cx - size / 2 - 6}" y="${cy + size / 2 + 14}" width="${size + 12}" height="1" fill="${STROKE}"/>
${text(label, cx, cy + size / 2 + 40, 26, WHITE)}`;
}
function header(title: string, subtitle: string): string {
return `${text(title, 960, 108, 56, WHITE, "bold")}
${text(subtitle, 960, 164, 28, MUTED)}`;
}
function footer(caption: string): string {
return `${text(caption, 960, 872, 24, MUTED)}`;
}
const keys: Record<string, string> = {
inflightIdle: renderInflight({ modelName: "DeepSeek-V4-Flash-0731", state: "ready", count: 0, offline: false }),
inflightActive: renderInflight({ modelName: "DeepSeek-V4-Flash-0731", state: "ready", count: 3, offline: false, history: [0, 1, 2, 3, 2, 3, 4, 3, 3, 2, 3, 2, 1] }),
usageP95: renderUsage({ modelName: "DeepSeek-V4-Flash-0731", stats: { totalRequests: 1950, totalInputTokens: 312177540, totalOutputTokens: 889193, genP95: 378.86 }, primaryStat: "gen_p95", offline: false }),
usageReq: renderUsage({ modelName: "All Models", stats: { totalRequests: 1985, totalInputTokens: 312327705, totalOutputTokens: 932710, genP95: 378.86 }, primaryStat: "requests", offline: false }),
gpuUtil: renderGpuGraph({ gpuName: "GPU 0 · RTX 5090", metric: "util_percent", value: 67, history: [10, 20, 40, 67, 55, 80, 72, 45, 60], offline: false }),
gpuPower: renderGpuGraph({ gpuName: "GPU 1 · RTX 5090", metric: "power", value: 312, history: [100, 150, 200, 312, 280, 350, 220, 190, 260], offline: false }),
gpuAll: renderGpuGraph({ gpuName: "ALL GPUS", metric: "temperature", value: 63, history: [55, 58, 61, 63, 60, 65, 64, 62, 66], offline: false }),
gpuCombo: renderGpuGraph({ gpuName: "GPU 0+2", metric: "util_percent", value: 71, history: [30, 45, 52, 71, 64, 78, 70, 58, 66], offline: false }),
};
const pngs: Record<string, string> = {};
for (const [name, svg] of Object.entries(keys)) {
pngs[name] = rasterizeKey(name, svg);
}
function writePng(name: string, w: number, h: number, body: string): void {
const svgPath = path.join(TMP, `${name}.svg`);
writeFileSync(svgPath, svg(w, h, body));
convert(svgPath, path.join(OUT, name), w, h);
console.log(`wrote store-assets/${name} (${w}x${h})`);
}
// Thumbnail — 1920x960
{
const s = 240;
const cx = [1250, 1570];
const cy = [360, 700];
const cols = [
keyWithLabel(pngs.inflightActive, "In-Flight Monitor", cx[0], cy[0], s),
keyWithLabel(pngs.usageReq, "Usage stats", cx[1], cy[0], s),
keyWithLabel(pngs.gpuUtil, "GPU Graph", cx[0], cy[1], s),
keyWithLabel(pngs.gpuCombo, "GPU combination", cx[1], cy[1], s),
];
const bullets = [
["#3fae5a", "In-flight request counts with a 60-second activity spark"],
["#d9a02a", "Usage stats: requests, tokens, and generation-speed P95"],
["#e0453a", "Live GPU graphs — per GPU, all GPUs, or a combination"],
]
.map(
([color, t], i) =>
` <circle cx="116" cy="${480 + i * 78}" r="10" fill="${color}"/>\n` +
text(t, 146, 488 + i * 78, 28, WHITE, "normal", "start"),
)
.join("\n");
writePng(
"thumbnail.png",
1920,
960,
`${text("llama-watch", 116, 250, 96, WHITE, "bold", "start")}
${text("Stream Deck keys for your llama-swap LLM server", 116, 322, 30, MUTED, "normal", "start")}
<rect x="116" y="350" width="620" height="3" fill="${GREEN}"/>
${bullets}
${cols.join("\n")}`,
);
}
// Gallery 1 — In-Flight Monitor
{
const s = 240;
const centers = [470, 810, 1150, 1490];
const items = [
[pngs.inflightIdle, "IDLE — no requests"],
[pngs.inflightActive, "3 in flight + 60s spark"],
[pngs.usageP95, "Usage · gen speed P95"],
[pngs.usageReq, "Usage · totals"],
];
writePng(
"gallery-1-inflight.png",
1920,
960,
`${header("In-Flight Monitor", "Live request counts, a 60-second activity spark, and usage stats")}
${items.map(([png, label], i) => keyWithLabel(png as string, label as string, centers[i], 460, s)).join("\n")}
${footer("Counts update instantly from the SSE feed; the spark shows the last 60 seconds of activity.")}`,
);
}
// Gallery 2 — GPU Graph
{
const s = 240;
const centers = [470, 810, 1150, 1490];
const items = [
[pngs.gpuUtil, "GPU 0 · utilization"],
[pngs.gpuPower, "GPU 1 · power draw"],
[pngs.gpuAll, "All GPUs · temperature"],
[pngs.gpuCombo, "GPU 0+2 · utilization combo"],
];
writePng(
"gallery-2-gpu.png",
1920,
960,
`${header("GPU Graph", "Live metric charts — one GPU, all GPUs, or a custom combination")}
${items.map(([png, label], i) => keyWithLabel(png as string, label as string, centers[i], 460, s)).join("\n")}
${footer("Sampled every 5 seconds: utilization, VRAM, temperature, power draw, and fan speed.")}`,
);
}
// Gallery 3 — Setup
{
const field = (label: string, value: string, y: number): string =>
`${text(label, 200, y, 22, MUTED, "normal", "start")}
<rect x="200" y="${y + 22}" width="700" height="44" rx="10" fill="${BG}" stroke="${STROKE}" stroke-width="2"/>
${text(value, 220, y + 22 + 29, 22, WHITE, "normal", "start")}`;
const panel = `${text("In-Flight Monitor settings", 200, 240, 30, WHITE, "bold", "start")}
${field("Base URL", "http://localhost:9292", 300)}
${field("API Key", "Optional", 395)}
${field("Model", "DeepSeek-V4-Flash-0731 (ready)", 490)}
${field("Display", "Usage stats", 585)}
${field("Primary stat", "Generation speed P95", 680)}
<rect x="160" y="170" width="760" height="640" rx="24" fill="${PANEL}" stroke="${STROKE}" stroke-width="3"/>`;
writePng(
"gallery-3-setup.png",
1920,
960,
`${header("Point it at your llama-swap instance", "Per-key settings in the property inspector")}
<g transform="translate(0,0)">${panel}</g>
${keyWithLabel(pngs.usageReq, "Usage stats key", 1420, 430, 300)}
${footer("Every key is independent: model, display mode, GPU, metric, and combination.")}`,
);
}
// App icon — 288x288
convert(path.join(ROOT, "com.bryce.llamawatch.sdPlugin/imgs/plugin/icon.svg"), path.join(OUT, "app-icon-288.png"), 288, 288);
console.log("wrote store-assets/app-icon-288.png (288x288)");