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
This commit is contained in:
@@ -0,0 +1,188 @@
|
||||
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)");
|
||||
Reference in New Issue
Block a user