dsh-local-studio
Local creation studio for DeepSeek Harness: a sidebar panel that turns ComfyUI into a text-to-image button and GPT-SoVITS into a text-to-speech button, and keeps every generated file in a local folder.
Everything runs on your machine. The plugin never uploads prompts, images or audio, and it never calls a cloud API.
Features
- Sidebar entry + full-screen panel — React rendered directly (no iframe), dark/light aware
(uses DSH theme tokens),
Esccloses it. - Health dots — ComfyUI and GPT-SoVITS are probed with a 3 s timeout; hover a dot to see
the exact failure (
connection refused,timeout, …) and the fix. - Image tab — prompt / width / height / steps → ComfyUI
/prompt→ poll/history/{id}→ download/view→ saveoutputs/local-studio/image-<timestamp>.png, preview in the panel. - Speech tab — text (+ optional reference audio) → GPT-SoVITS
/tts(GET, then POST as a fallback) → savevoice-<timestamp>.wav, listen directly in the panel. - Recent outputs tab — the 30 newest images/audio files in the output directory, with thumbnail / audio preview and copy-path.
- Friendly errors everywhere — every route returns
{ "error": "...", "hint": "..." }with a 4xx/5xx status; a stopped ComfyUI or a bad checkpoint name never crashes the host.
Requirements
| Component | Version | Notes |
|---|---|---|
| DeepSeek Harness | >= 0.2.2-rc.1 < 0.3.0-0 |
uses the connection Fetch-route service |
| Node.js | >= 20 |
DSH runtime already satisfies this |
| ComfyUI | optional | default http://127.0.0.1:8188 |
| GPT-SoVITS | optional | its api.py server, default http://127.0.0.1:9880 |
Both services are optional: without them the panel still opens and tells you what to start.
The default ComfyUI workflow uses Animagine XL 3.1 (animagine-xl-3.1.safetensors); any
SDXL checkpoint works if you set comfyCheckpoint or supply your own workflow.
Install
# from the plugin market (once published), or from a packed tarball:
npm pack
dsh plugin --profile desktop add ./dsh-local-studio-0.1.0.tgz
For local development you can link the directory instead (see ai-brain/dsh-plugin/install.mjs
for the link-based recipe): copy this folder into <profile>/local-plugins/dsh-local-studio,
add link:./local-plugins/dsh-local-studio to the profile dependencies, append
dsh-local-studio to dsh.profile.bundles, run pnpm install, then restart DSH.
After a restart the sidebar footer shows 🎨 Local Studio; open it from there.
Configuration
~/.dsh/local-studio.config.json is optional — when it is missing, these defaults are used and
no file is created:
{
"comfyUrl": "http://127.0.0.1:8188",
"comfyCheckpoint": "animagine-xl-3.1.safetensors",
"comfyWorkflow": "",
"ttsUrl": "http://127.0.0.1:9880",
"ttsRefAudio": "",
"ttsPromptText": "",
"ttsPromptLang": "zh",
"outputDir": ""
}
| Key | Meaning |
|---|---|
comfyUrl |
ComfyUI root URL. Set to "" to disable the image route (it answers 501). |
comfyCheckpoint |
ckpt_name for CheckpointLoaderSimple — must match a file under ComfyUI/models/checkpoints. |
comfyWorkflow |
Optional absolute path to a ComfyUI API-format workflow JSON. Supports the placeholders below. |
ttsUrl |
GPT-SoVITS root URL. Set to "" to disable the speech route (it answers 501). |
ttsRefAudio |
Optional default reference audio path for GPT-SoVITS. |
ttsPromptText / ttsPromptLang |
Optional prompt_text / prompt_lang for the reference audio. |
outputDir |
Output folder. Empty = <workspace>/outputs/local-studio. |
Environment overrides (used by the tests and handy for debugging):
DSH_LOCAL_STUDIO_CONFIG— path to the config fileDSH_LOCAL_STUDIO_OUTPUT— overrideoutputDirDSH_WORKSPACE— override workspace detection
The built-in workflow (also shipped as app/workflow.json) supports these placeholders:
{{PROMPT}}, {{NEGATIVE}}, {{WIDTH}}, {{HEIGHT}}, {{STEPS}}, {{SEED}},
{{CHECKPOINT}}. A custom workflow may use them or hard-code everything.
HTTP API
All routes live on the authenticated shared /api channel of DSH (connection.fetch.register),
so the Host/Origin fence and login state are enforced before the handler runs. Every response is
JSON except /file; failures are { "error": "...", "hint": "..." } plus a 4xx/5xx status.
GET /api/dsh-local-studio/health
{
"ok": false,
"comfy": { "ok": false, "detail": "/system_stats → 连接被拒绝(服务可能没启动);/queue → …", "hint": "…", "url": "http://127.0.0.1:8188" },
"tts": { "ok": true, "detail": "HTTP 200 · 根路径可达", "url": "http://127.0.0.1:9880" },
"config": { "comfyUrl": "…", "ttsUrl": "…", "comfyCheckpoint": "…", "outputDir": "…", "configFile": "…", "configExists": false },
"warnings": [],
"version": "0.1.0"
}
Both probes time out after 3 s.
POST /api/dsh-local-studio/image
Request: { "prompt": "1girl, cherry blossoms", "width": 1024, "height": 1024, "steps": 28 }
(width/height must be multiples of 8, 256–2048; steps 1–80; seed optional).
Response: { "file": "C:\\…\\image-20261108-135732.png", "name": "image-20261108-135732.png", "width": 1024, "height": 1024, "bytes": 123456, "promptId": "…", "elapsedMs": 8123, "seed": 123456 }
Flow: POST /prompt → poll GET /history/{prompt_id} every 1 s for up to 90 s → download
GET /view → save the PNG into outputDir.
POST /api/dsh-local-studio/tts
Request: { "text": "你好,世界", "refAudio": "D:\\voices\\ref.wav" } (refAudio optional;
text ≤ 2000 chars).
Response: { "file": "…/voice-20261108-135800.wav", "name": "…", "bytes": 123456, "mediaType": "audio/wav", "chars": 6, "ms": 1830, "mode": "GET" }
Calls GET {ttsUrl}/tts?text=…&text_language=zh&text_lang=zh&media_type=wav…; if that endpoint is
not accepted (404/405/422) it retries with POST {ttsUrl}/tts and a JSON body. When ttsUrl is
explicitly empty the route answers 501 with a configuration hint.
GET /api/dsh-local-studio/outputs
{ "dir": "…\\outputs\\local-studio", "count": 3, "limit": 30, "files": [ { "name": "image-…png", "path": "…", "size": 123, "mtimeMs": 1762…, "kind": "image", "ext": "png" } ] }
Only .png/.jpg/.jpeg/.webp/.wav/.mp3/.ogg/.flac files are listed, newest first, capped at 30.
GET /api/dsh-local-studio/file?name=image-…png
Returns the raw bytes with the right content-type. Only bare file names inside outputDir are
accepted (.., path separators and control characters are rejected with 400).
Panel
| Tab | What it does |
|---|---|
| 🖼️ Image | prompt + width/height/steps, Generate, live elapsed timer, preview, copy path |
| 🔊 Speech | text + optional reference audio, Generate, inline <audio> player, copy path |
| 🗂️ Recent outputs | list of the 30 newest files, click to preview image/audio, copy path |
The header shows two health dots (ComfyUI / TTS) and a refresh button; the footer shows the resolved output directory.
Build & test
node build.mjs # gzip-embeds app/workflow.json -> lib/workflow.data.js,
# inlines app/panel.css -> lib/client.js, checks name consistency
npm test # smoke + unit + mock
npm run pack:check # npm pack --dry-run
test/smoke.mjs— offline structural test: the built client loads with a stubbed__ModuleLoader__, both slots register and render, the host module imports and registers five routes, the embedded gzip workflow decompresses,package.json/cordis.patch.yml/ icon / README / LICENSE are consistent.test/unit.mjs— no-network unit test:/healthreturns friendly{ok:false,detail}when the services are down,/outputsordering/limits, parameter validation, 501 for a disabled TTS, path-traversal protection, placeholder substitution, PNG header parsing.test/mock.mjs— boots a loopback mock of ComfyUI + GPT-SoVITS and exercises the whole chain: health probe, workflow submission, history polling, image download and save, TTS GET and POST fallback,node_errorsandstatus_str=errorfriendly errors, outputs listing and/file.
Not verified against a real ComfyUI / GPT-SoVITS in this repository: the mock covers the wire protocol and the error branches, but model names, generation times and GPU behaviour need a real box. If something fails, open the panel health dot tooltip first — it names the URL and the underlying socket error.
Known limitations
- One image per request; batch/queue management is left to ComfyUI.
- The built-in workflow is a single SDXL text-to-image graph; use
comfyWorkflowfor anything else (LoRA, ControlNet, upscale, video…). - Prompt length capped at 2000 characters, speech text at 2000 characters.
- The image route waits synchronously for up to 90 s; the panel shows a live timer.
- Windows-first development (output paths are shown as returned by the host).
License
MIT — see LICENSE.
No comments yet. Be the first to write one.