diff --git a/.gitignore b/.gitignore index 21aae6b..30ac3f3 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ node_modules/ *.streamDeckPlugin .DS_Store com.bryce.llamawatch.sdPlugin/bin/ +store-assets/*.png +store-assets/.gen/ diff --git a/com.bryce.llamawatch.sdPlugin/manifest.json b/com.bryce.llamawatch.sdPlugin/manifest.json index 7cb39b0..283f491 100644 --- a/com.bryce.llamawatch.sdPlugin/manifest.json +++ b/com.bryce.llamawatch.sdPlugin/manifest.json @@ -2,7 +2,7 @@ "$schema": "https://schemas.elgato.com/streamdeck/plugins/manifest.json", "Name": "llama-watch", "Version": "1.0.0.0", - "Author": "Bryce Zuccaro", + "Author": "c4ch3c4d3", "Actions": [ { "Name": "In-Flight Monitor", @@ -56,4 +56,4 @@ "Version": "24" }, "UUID": "com.bryce.llamawatch" -} +} \ No newline at end of file diff --git a/scripts/store-assets.mts b/scripts/store-assets.mts new file mode 100644 index 0000000..b4ddf03 --- /dev/null +++ b/scripts/store-assets.mts @@ -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 ` + +${body} +`; +} + +function text(content: string, x: number, y: number, size: number, fill: string, weight = "normal", anchor = "middle"): string { + return ` ${content}`; +} + +function keyImage(png: string, cx: number, cy: number, size: number): string { + return ` `; +} + +function keyWithLabel(png: string, label: string, cx: number, cy: number, size: number): string { + return `${keyImage(png, cx, cy, size)} + +${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 = { + 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 = {}; +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) => + ` \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")} + +${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")} + +${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)} + `; + writePng( + "gallery-3-setup.png", + 1920, + 960, + `${header("Point it at your llama-swap instance", "Per-key settings in the property inspector")} +${panel} +${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)"); diff --git a/store-assets/listing.md b/store-assets/listing.md new file mode 100644 index 0000000..5a2de3c --- /dev/null +++ b/store-assets/listing.md @@ -0,0 +1,75 @@ +# Marketplace Listing — llama-watch + +Copy the content below into Maker Console when creating the product. + +## Product type + +Stream Deck plugin + +## Name + +`llama-watch` + +## Author / Organization + +Organization: `c4ch3c4d3` (create this in Maker Console; sign the Maker Agreement) + +## Description + +> Copy exactly (254 chars first 250-char segment is unformatted; total ~900 chars, within the 250–1500 limit): + +``` +llama-watch brings live monitoring of your llama-swap LLM server to your Stream Deck. Watch in-flight request counts per model with a real-time activity spark, switch to Usage stats to see request totals, processed and generated tokens, and the generation-speed P95 — per model or across all models. GPU Graph keys render live line charts of GPU utilization, VRAM, temperature, power draw, and fan speed — for one GPU, all GPUs, or a custom combination. + +Features +- In-Flight Monitor: live per-model request count with a 60-second activity spark; Usage stats display (requests, processed/generated tokens, generation-speed P95) per model or all models. +- GPU Graph: live charts for utilization, VRAM, temperature, power draw, and fan speed — single GPU, all GPUs, or a custom combination. +- Per-key configuration: base URL and optional API key; every key is independent. + +Requirements +- A running llama-swap instance (open source) and its base URL — default http://localhost:9292. +- Optional API key for protected instances. +- macOS 13 or later; Stream Deck 7.1 or later. + +Privacy: the plugin reads data only from your configured llama-swap server and sends nothing elsewhere. No analytics, no telemetry. +``` + +## Tags + +`monitoring`, `gpu`, `llm`, `llama-swap`, `developer tools` + +## Price + +Free + +## Release notes (v1.0.0) + +``` +Initial release. + +- In-Flight Monitor: live per-model request count with a 60-second activity spark; Usage stats display (requests, processed/generated tokens, generation-speed P95) per model or all models. +- GPU Graph: live charts for utilization, VRAM, temperature, power draw, and fan speed — single GPU, all GPUs, or a custom combination. +- Per-key base URL and optional API key settings. +``` + +## Media checklist (generated by scripts/store-assets.mts) + +- `store-assets/app-icon-288.png` — app icon, 288×288 PNG +- `store-assets/thumbnail.png` — thumbnail, 1920×960 PNG +- `store-assets/gallery-1-inflight.png` — gallery, 1920×960 PNG +- `store-assets/gallery-2-gpu.png` — gallery, 1920×960 PNG +- `store-assets/gallery-3-setup.png` — gallery, 1920×960 PNG + +## Submission steps (Maker Console) + +1. maker.elgato.com → create organization `c4ch3c4d3` → sign the Maker Agreement. +2. Home → Create product → Stream Deck plugin. +3. Upload `com.bryce.llamawatch.streamDeckPlugin` (rebuilt with Author `c4ch3c4d3`). +4. Details: name `llama-watch`, description above, tags, price Free. +5. Media: upload the 5 PNGs above as app icon + thumbnail + 3 gallery items. +6. Release notes: v1.0.0 above. Submit. +7. Review: allow 4–10 business days; handle feedback by submitting a revision. + +Notes +- The product name and monetization cannot be changed after creation without contacting maker@elgato.com — confirm `llama-watch` is still available when you create it. +- No demo video is required (the plugin integrates with the user's own software, not a paid service/hardware).