Kevin Yoder
AI / ML Tooling

Self-Hosted Generative-AI Studio

One prompt, many outputs — a self-hosted toolkit that turns a sentence into text, images, music, speech, or a 3D mesh across a small multi-machine GPU fleet.

Built Feb–Apr 2026 Multi-machine · LAN only Write-up — parts dormant

⚙ How it works ▦ Results 🖼 Screenshots

01 Overview

Not scattered apps but one toolkit: a self-hosted, multi-machine generative-AI studio that turns a prompt into text, images, music, speech, a 3D mesh, or an Office document. Two web front-ends sit over a spine of Dockerized model servers and cloud APIs, with a custom ComfyUI node pack wiring the same fleet into node graphs. It was built to sit on top of the LiteLLM / Hugging Face / GPU plumbing already running on my home server, and to let autonomous pipelines generate their own assets.

Orchestrator

AssetForge

Express + Socket.IO app: one prompt across 5 pipelines (text · image · audio · 3D · document) and ~20 output formats, with a persistent SQLite job queue, live logs, and a gallery.

Playground

AI Playground

A Fastify multi-task ML playground — 19 task types over ~130 curated model entries spanning Hugging Face, LiteLLM (Claude · Gemini · GPT · Ollama), and the local GPU services.

Model spine

Model servers

Small Flask / FastAPI wrappers that give heavyweight models a one-endpoint REST API: MusicGen, Shap-E, ACE-Step, TripoSR (image→3D), and Kokoro neural TTS.

Integration

ComfyUI nodes

Three first-party ComfyUI nodes (TTS · MusicGen · TripoSR) that let graphs on a GPU PC call the fleet over the LAN and return native audio tensors or GLB files.

02 Why I built it

The home server already had the pieces — an LLM gateway, Hugging Face access, a couple of small GPUs — but no single place to say "make me a thing." AssetForge and AI Playground are that unifying front-end over plumbing that was already configured. The second motive was to feed my browser-game generation platform: a summon_asset CLI and MCP server let build pipelines call the studio programmatically, so an automated game build can synthesize its own sprites, sound effects, music, and 3D models. And some of it is simply family-facing — a shared story generator reads its output aloud through the same TTS stack. Cost and locality run through all of it: local GPU services are labelled "free," free-tier cloud models are preferred where they suffice, and there are deliberate CPU fallbacks for when the small server GPU can't cope.

03 What I built & how it works

Two front-ends, a spine of model servers, and a health poller that prefers the fast path.

Programmatic callers summon_asset CLI · MCP server · web-game-studio auto-asset loop home server — orchestrator Ubuntu · GTX 960M 2 GB AssetForge · AI Playground · TTS Studio · StoryForge SQLite queue · one job at a time · health poller GPU PC · Windows Windows · GTX 1080 Ti TripoSR image→3D (~10s) Kokoro TTS · ComfyUI · 3 nodes LAN glb pipeline dispatch · health-gated fallbacks 5 asset pipelines · health-gated backend routing text/docs → LiteLLM · Claude · Gemini · GPT · Ollama image → HF router · DALL·E 3 · ComfyUI audio → MusicGen · ACE-Step 3D → Shap-E · TripoSR · Tripo3D · Meshy speech → Kokoro · Gemini TTS laptop RTX 3080 — dormant Juice GPU-over-IP node · built, not wired in — present, not in the routing path

Fig. 1 — the front-ends orchestrate; the GPU PC handles image→3D and neural TTS; a background poller routes to whichever backend is awake.

  1. Describe — a prompt, an asset type, a backend, and per-format options rendered from a server-side capability matrix.
  2. Queue — one UUID job row in SQLite, processed a single job at a time to avoid resource contention on shared hardware.
  3. Dispatch — route to the right pipeline; a 5-minute health poll prefers the local GPU when it's awake and cloud APIs when it isn't.
  4. Generate — the model server or API runs; heavyweight models fall back to CPU mid-request rather than fail on the 2 GB card.
  5. Stream & store — Socket.IO logs stream live and replay for late-joining subscribers; the output and its full log land in the job row.

04 🛠 Skills & tech used

Languages
Node.js (CommonJS + ESM)Python 3SQL / SQLite (FTS5)BashHTML / CSS (no build step)
ML / AI
LiteLLM orchestrationHF Transformers / Diffusers servingMusicGen · Shap-E · ACE-StepTripoSR image→3DKokoro · Gemini neural TTSComfyUI API workflowsGemini multimodal (audio · image)
Infra / Ops
Docker Compose (multi-file)per-model healthchecks & mem_limitssystemd on WSL2GPU-over-IP (Juice)USB model-cache + symlink tricksuv Python installs
Frontend
Socket.IO live-log streamingSSE + AbortControllervanilla-JS SPAsdrag-drop / base64 uploadserver-declared form schemas
Data
content-hash response cacheusage analytics + monthly capsFTS5 model searchSQLite WAL tuning
Techniques
graceful-degradation fallback chainssync façade over async APIssingle-concurrency job queuecapability-driven UIdependency-pin archaeology

05 Notable challenges & decisions

Most of the work was fitting real models onto small, shared hardware without the UI ever knowing.

Small GPUs

Engineering around a 2 GB server GPU

MusicGen runs in fp16, but Shap-E has to fall back to fp32 because the Maxwell-era 960M can't do half-precision LayerNorm. Every model server has a CUDA-OOM → CPU retry mid-request, and MusicGen exposes an /unload endpoint purely to hand VRAM back to a sibling service. ACE-Step is CPU-only — the server's driver can't reach the CUDA version it needs — with a bfloat16 source patch that roughly halved its RAM, from ~10 GB to ~5.3 GB.

Async APIs

A sync façade over an async model server

ACE-Step's real API is submit / poll / download. The wrapper spawns it as a subprocess, waits for the model to finish downloading, then hides the whole submit→poll→fetch dance inside one blocking /generate call — so AssetForge can treat it exactly like the simpler servers. The same sync-over-async pattern wraps the Tripo3D and Meshy cloud pollers.

Fallbacks

Prefer the fast path, degrade quietly

A 5-minute health poller lets pipelines silently prefer the GPU PC — ComfyUI for images, TripoSR for 3D — when it's awake, and fall back to cloud APIs when it isn't. The honest trade-off is that when a container has actually been removed, the request just returns a 503 rather than pretending.

Streaming

SSE over a home LAN is fragile

The story generator needed six separate fixes to keep token streams alive: close-on-response rather than request, a compression filter that skips text/event-stream, keepalive / no-delay sockets, and 15-second heartbeat comments so a NAT or proxy doesn't drop the connection during the latency before the first token.

Deliberate constraints. The job queue processes one job at a time on purpose — most backends share one small GPU or CPU budget — and every backend and output format lives in a server-side capability matrix that the UI renders forms from, so adding a model is a server-only change.

06 Results

5
asset pipelines (text · image · audio · 3D · docs)
19
AI Playground task types
~130
curated model entries, local + cloud
~10s
photo → 3D GLB on the 1080 Ti (TripoSR)
3
first-party ComfyUI nodes
2GB
server GPU most models were squeezed onto

Sources: service source and README files, container health output, and dated task summaries. There is no git history for these services, so figures are reconstructed from those and file timestamps.

07 Screenshots

The AssetForge screenshots below were captured 2026-08-02 from the verified runnable clone (Steps to Build tab). The other studio surfaces are not part of the clone and remain placeholders.

AssetForge job history: a completed CPU music-generation job with a Download button, a 3D Shap-E job running, and a text job that failed cleanly because no LLM endpoint is configured.
Job history from the clone: a completed CPU music job, a 3D job mid-run, and a text job failing honestly with no LLM configured — the queue runs one job at a time by design.
AssetForge gallery showing two generated assets: a 3D-rendered rubber duck GIF preview and a generated WAV music clip.
The gallery after a key-free session: a Shap-E rubber duck rendered on CPU and a MusicGen piano clip.
AssetForge generate view: five asset-type tiles — text, image, audio, 3D, document.
The generate view: five pipelines behind one interface; forms render from server-declared capability matrices.
[ AI Playground · chat ]
provider-grouped model dropdown
The model list spans cloud providers, a laptop and a desktop GPU, and Juice-GPU entries — it tells the multi-machine story at a glance.
[ TTS Studio ]
flag-grouped voice browser
Neural voices grouped by language with inline playback, wrapping Kokoro on the GPU PC plus Gemini TTS.
[ ComfyUI ]
the three home-server nodes wired into a graph
Add Node → home-server: the custom nodes let a ComfyUI graph call TTS, MusicGen, and TripoSR across the LAN.

08 Honest status

This is one coherent toolkit built Feb–Apr 2026, described here as a write-up rather than a live demo. The two web front-ends, TTS Studio, and the story generator were up at the last survey; the MusicGen, Shap-E, and ACE-Step model containers were later deleted, though their sources remain. Update (2026-08-02): a runnable partial clone now exists (Steps to Build tab) — AssetForge plus rebuilt MusicGen and Shap-E services, verified end-to-end on CPU with no keys (a real music clip and a real 3D GIF), which also confirms the deleted containers rebuild cleanly from source. The Juice GPU-over-IP wiring is present but dormant; the compose files currently run on the local GPU instead. Everything is unauthenticated and LAN-only by design — a home-server convenience, not production security — and there is a known latent bug in one ComfyUI node's 3D path that I'd want to retest before demoing it. None of these services is a git repository, so the timeline and figures are reconstructed from dated task summaries and file timestamps rather than commit history.

genai-studio — Build Recipe

Take a bare machine to a running slice of a self-hosted generative-AI studio: AssetForge, a unified web app that generates text, images, music, 3D previews, and office documents (5 pipelines, ~30 backend entries, ~20 output formats including .docx/.pptx/.xlsx) through one interface — with a persistent job queue, live Socket.IO log streaming, a history browser, and a gallery — plus two bundled model microservices: MusicGen (text→music) and Shap-E (text→3D rotating-GIF preview). Key-free by default: the music pipeline works out of the box on CPU, no API keys, no GPU.

This is a partial clone of a larger personal studio — 3 of its 7 services ship here; the other 4 are deliberately left out (named and explained under Known limitations).

Status:Verified 2026-08-02 — built from this clone on a Linux Docker host (docker compose up -d --build). First boot clean (healthy containers, empty job list); a key-free 5-second music job ran end-to-end on CPU (queued → MusicGen-small download → generation in ~351 s total → real 316 KB PCM WAV downloaded via the API); a text job with no LLM configured failed honestly with the "LITELLM_URL not configured" message while the app stayed healthy; the single-concurrency queue behaved as designed; and with --profile 3d a Shap-E text→3D job produced a real rotating GIF on CPU (~11 min warm — the first-ever request also downloads ~3.4 GB of weights and needs the 30-minute budget documented below); and with LITELLM_URL pointed at an OpenAI-compatible endpoint the text pipeline generated a real markdown asset end-to-end (verified against a LiteLLM gateway with a disposable key, revoked after the test). Browser pass: SPA renders, the History and Gallery views show the generated assets, 0 console errors. The prebuilt tarball (docker save of all three images) size is recorded in ../PREBUILT_IMAGES.md. The clean clone lives in clone/.

Sensitive data: none. Post-rotation the sources were already key-free (env-only per-service keys); home-server/tailnet/LAN identifiers were scrubbed; no runtime databases or outputs ship — and the studio services that hold family data (StoryForge, TTS Studio) are deliberately not part of this clone.


What it is

Three containers (only the first two run by default):

Service Stack Role Port
assetforge Node 20 / Express + Socket.IO + better-sqlite3 Web UI, one-at-a-time job queue, 5 generation pipelines (text / image / audio / 3D / document), format converters (FFmpeg, sharp, docx/pptxgenjs/exceljs) 8212
music-gen Python / Flask + Transformers facebook/musicgen-small wrapper: /generate returns WAV; lazy model load, CUDA-OOM→CPU retry built in 8208
shap-e (opt-in, --profile 3d) Python / Flask + Diffusers openai/shap-e wrapper: /generate returns a rotating multi-frame GIF (~12 fps) 8209

The frontend is a hand-built vanilla-JS SPA (no build step); backend/format choices are server-declared capability matrices, so the UI renders its forms from the API. Job logs are buffered in SQLite and replayed to late-joining WebSocket subscribers.

Prerequisites

New machine? See ../SETUP.md. Then, per tier:

Tier You need
1 — prebuilt containers (recommended) Docker (see ../SETUP.md) + this bundle's clone/ folder (for the compose file).
2 — build from source Same as Tier 1.
3 — bare-metal Node 20+ and ffmpeg for AssetForge; Python 3.10+ with PyTorch for the model services (commands below).

Configuration — nothing required

docker compose up needs no .env and no keys. What works at each level:

Setting Unlocks Without it
(nothing) Music generation on CPU — the whole audio pipeline, WAV/MP3/FLAC/OGG
--profile 3d Local Shap-E text→3D GIF previews (key-free, CPU). First 3D request is slow: it downloads ~3.4 GB of weights and loads the model (~4 min on CPU) before the ~7-minute inference — the pipeline's 30-minute budget (SHAP_E_TIMEOUT_MS) covers this; later requests skip straight to inference. Submit with "assetType":"threed". 3D jobs fail with a clear "start it with --profile 3d" message
LITELLM_URL (+ key/model) Text + Document pipelines (md/html/docx/pptx/xlsx) and the blender-llm 3D backend — any OpenAI-compatible endpoint; local Ollama works key-free those jobs fail with a clear "not configured" message
HF_TOKEN or OPENAI_API_KEY Image pipeline (FLUX.1-Schnell / SDXL via HF, or DALL-E 3) image jobs fail with "token not configured"
TRIPO_API_KEY / MESHY_API_KEY / COMFYUI_URL / TRIPOSR_URL / BLENDER_HUB_URL / ACE_STEP_URL Cloud image→3D, an external ComfyUI/TripoSR/BlenderHub/ACE-Step backends stay listed but report down / a clear error

All documented one line at a time in clone/.env.example.


Tier 1 — Run the prebuilt containers (recommended)

Availability: the prebuilt images are available on request — they are not published or linked anywhere. Ask Kevin for them, or build from source via Tier 2.

docker load -i genai-studio-clone-image.tar   # loads all three images
cd clone
docker compose up -d                          # → http://localhost:8212  (no --build)

To produce the tarball on a machine that has built the images (Tier 2): docker save genai-studio-assetforge genai-studio-music-gen genai-studio-shap-e -o genai-studio-clone-image.tar. The model-service images are honestly large — they carry a full PyTorch base.

Tier 2 — Build from source

cd clone
docker compose up -d --build                # → http://localhost:8212
curl -s localhost:8212/health               # → {"status":"ok","service":"assetforge",...}
curl -s localhost:8208/health               # → {"loaded":false,"model":"facebook/musicgen-small","status":"ok",...}

Open http://localhost:8212AudioMusicGen Small (Local) → WAV → set duration ~10 s → a prompt like "upbeat lo-fi hip-hop with jazz chords"Generate, and watch the live log stream. The first request downloads and loads the model (a few minutes); after that, expect roughly real-time×8–20 on CPU — ~4–5 minutes for a 30-second clip, proportionally less for short clips. The app boots with an empty job list; History and Gallery fill as you generate.

Tier 3 — Bare-metal (three processes)

AssetForge (needs Node 20+ and ffmpeg on PATH):

cd clone/assetforge
npm install --omit=dev
DATA_DIR=./data OUTPUT_DIR=./outputs TEMP_DIR=./temp \
  MUSIC_GEN_URL=http://localhost:8208 SHAP_E_URL=http://localhost:8209 \
  node server.js                            # → http://localhost:8212

music-gen / shap-e (Python 3.10+; CPU-only PyTorch is fine):

cd clone/music-gen           # or clone/shap-e
pip install torch --index-url https://download.pytorch.org/whl/cpu
pip install -r requirements.txt
python app.py                               # :8208 (music-gen) / :8209 (shap-e)

On Windows, set the same variables with $env: before node server.js.

Optional extras (all off by default)

  • Shap-E text→3Ddocker compose --profile 3d up -d --build. Key-free and CPU-capable, but slow: ~5–7 minutes per GIF at the low end (16 steps, small frame size; CPU-dependent — the verified host measured ~7); the UI's default 64 steps takes proportionally longer. It auto-uses an NVIDIA GPU if you uncomment the deploy block in docker-compose.yml (fp16 on modern GPUs, fp32 on older ones — the service picks by compute capability).
  • LLM text + documents — point LITELLM_URL at any OpenAI-compatible endpoint. Local key-free path with Ollama:

bash # in clone/.env LITELLM_URL=http://host.docker.internal:11434 # no /v1 — the app appends /v1/chat/completions # (Linux: if host.docker.internal doesn't resolve, use your host's LAN IP) LITELLM_API_KEY=ollama LITELLM_MODEL=llama3.2 # forces one model for all text/document backends

With a LiteLLM gateway that defines the app's model aliases (claude-sonnet, gemini-flash, …) you can leave LITELLM_MODEL unset and the per-backend dropdown routes to different models. - ImagesHF_TOKEN (HuggingFace, free tier exists) unlocks FLUX.1-Schnell and SDXL; OPENAI_API_KEY unlocks DALL-E 3. - External 3D/image servicesCOMFYUI_URL (image jobs silently prefer a healthy ComfyUI over HF), TRIPOSR_URL (image→GLB), BLENDER_HUB_URL (LLM-written Blender scripts — pairs with the blenderhub recipe), ACE_STEP_URL (long-form music, 10–600 s), TRIPO_API_KEY/MESHY_API_KEY (cloud image→GLB). Each is health-gated or fails with a clear message.

Verify

curl -s localhost:8212/health                       # queue state, uptime
curl -s localhost:8212/api/backends/audio           # backend matrix (server-declared)
curl -s localhost:8208/health                       # music-gen up
# end-to-end key-free music job (short clip to keep CPU time reasonable):
curl -s -X POST localhost:8212/api/jobs -H 'Content-Type: application/json' \
  -d '{"assetType":"audio","backend":"music-gen","format":"wav",
       "prompt":"calm piano melody","options":{"duration":5}}'
# poll GET /api/jobs/<jobId> until "status":"completed", then download:
#   GET /api/assets/<jobId>/download

Tests (assetforge ships a small node:test suite with mocked HTTP): cd clone/assetforge && npm install && node --test test/local-backends.test.js — 3 tests, all passing as shipped.

What was stripped from the personal version (de-identification + minimalism)

  • The remote-GPU backends. The original studio could reach a GPU PC over the LAN for ComfyUI, TripoSR, and Bark (LAN IPs set via compose env). Those backends stay in the capability matrix but now default to unset env vars — the health poller simply reports them down and jobs fail with a clear message instead of dialing someone's home network.
  • The Juice GPU-over-IP shim. Both model services shipped a juice-entrypoint.sh that LD_PRELOADed a remote-GPU libcuda.so (plus the libatomic1 package it needed). Removed entirely — the services keep their existing built-in CUDA-detect / OOM→CPU fallback and just run python app.py.
  • The hard LiteLLM coupling. Text/document generation now activates only when LITELLM_URL is set (any OpenAI-compatible endpoint; a new LITELLM_MODEL override handles endpoints without the personal gateway's model aliases). No LLM gateway container is bundled.
  • Identifying strings in the AssetForge README: a LAN IP, a Tailscale hostname, home-server storage paths (/mnt/...), and "built for " wording → localhost URLs, container paths, neutral wording. GPU-node labels → neutral. Comments referencing "this homeserver" → generic.
  • No runtime data ships. Fresh named volumes; the app boots with an empty job list, and the model caches download on first use.
  • Family data was never at risk here — the studio's story-generator and TTS-history databases (which do contain family names) belong to services that are not part of this clone (see below).

Bugs found and fixed during cloning (faithful, minimal fixes)

  1. Stale TripoSR test — the shipped test mocked the TripoSR wrapper returning raw GLB bytes, but the pipeline had since moved to a JSON {mesh_url} + separate-download contract, so the test failed against the code next to it. The mock now matches the real contract (verified: 3/3 tests pass).
  2. README port drift — the original README's architecture diagram and access URLs said :8211; the service has always listened on :8212. Corrected (the README is user-facing — it renders in-app on the Docs page).

Known limitations (stated honestly)

  • Personal-LAN tool: no authentication on any endpoint, and Socket.IO CORS is *. Put it behind a reverse proxy with auth before exposing it.
  • One job at a time, by design. The queue is deliberately single-concurrency (the original shared one small GPU/CPU budget). Also, the in-memory queue does not survive restarts: jobs stuck running are marked failed at startup, and pending rows are not re-enqueued — the original's "no jobs are lost" comment overstates it.
  • CPU generation is slow. MusicGen: ~4–5 min per 30 s clip (clips clamp to 5–30 s; short clips are the sane default). Shap-E: ~5–7 min per GIF at minimum settings (CPU-dependent), more at the UI defaults. These are real model inference times, not bugs.
  • The image pipeline needs a key. There is no key-free image backend — without HF_TOKEN/OPENAI_API_KEY/COMFYUI_URL, image jobs fail with a clear message. Note the hf-sd15 menu entry actually maps to SDXL (HF delisted SD 1.5 upstream; the original absorbed that).
  • 4 of the studio's 7 services are not cloned, deliberately:
  • AI Playground (19-task ML playground, ~130 curated models) — key-free it reduces to dashboards and avatar generation; nearly every interesting task needs HF/LLM credentials, so a clone would demo mostly empty.
  • TTS Studio and StoryForge (voice browser; family story generator) — their cores require an LLM/TTS stack, and their histories/databases carry family data. Cleanest de-identification was to not ship them.
  • ACE-Step (full-quality music with lyrics) — a ~5 GB model download and a heavyweight patched build for minutes-long CPU generations; too heavy for a reasonable demo clone. Its backend entry remains and works if you point ACE_STEP_URL at your own instance.
  • A hidden bark audio backend exists in the pipeline (not in the UI) for a Gradio tts-generation-webui — kept as-is, off unless TTS_GEN_URL is set.
  • The containers run as root (as the original did); hardening is left to the deployer.