Quickstart
From a clone of helmstudio to a studio of your own, running under helm dev and recording its first output.
There is no release yet, and none of the packages is on PyPI or npm, so this builds
helmfrom source and installs the runtime SDK from the clone.
You need Go 1.27.1 or newer, Python 3.9 or newer, git and curl, and a network connection to clone, build and install. Nothing here needs a GPU, a model or a Hugging Face account.
1. Clone helmstudio
git clone https://github.com/janishar/helmstudio
cd helmstudio
2. Build helm
helm validates manifests, and runs a studio with no daemon. Go downloads the modules helmstudio depends on the first time it builds.
go build -o bin/helm ./cmd/helm
3. Make a Python environment
A studio runs in its author's environment. helm dev never makes one, or changes one.
python3 -m venv .venv
source .venv/bin/activate
4. Install the runtime SDK
The Python client uses the standard library and nothing else. Installing it fetches pip's build tools.
pip install ./packages/helm-runtime-sdk/python
5. Copy the example studio
The example is about the smallest studio that does something: it makes an image from a prompt, and records it.
cp -R site/samples/hello-studio ../hello-studio
cd ../hello-studio
Its manifest says what it is, what it asks helmstudio for, and how to run it:
# A studio helmstudio can run: one process, asking for two capabilities.
id: hello-studio
name: hello studio
description: Makes a small image from a prompt, and records it with its parameters.
kinds: [image]
license: MIT
# Where the studio's repository will live. helm dev runs the checkout it is
# given, and never clones this.
repo: https://github.com/you/hello-studio
requires:
os: [darwin, linux]
arch: [arm64, amd64]
tools: [python3]
ram_gb: 1
disk_gb: 1
peak_ram_gb: 1
runtime:
framework: other
backends: [cpu]
sdk: { runtime: "^1" }
capabilities: [assets, gallery]
processes:
- name: studio
role: main
# helm dev runs this in the manifest's own directory, and gives it a free
# port, preferring 8765.
cmd: "python3 studio.py --port {port}"
port: { prefer: 8765 }
health: { path: /healthz, timeout_s: 30 }
ui: /
Its code is a web server that uses the runtime SDK twice: to adopt the file it made, and to record a gallery item with the parameters that made it.
"""hello studio: makes a small image from a prompt, and records it.
Everything that touches helmstudio goes through the runtime SDK's client:
adopting the file the studio wrote, and recording a gallery item with the
parameters that made it. The rest is the studio's own business.
"""
import argparse
import hashlib
import json
import os
import struct
import zlib
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from helm_runtime_sdk import from_env
# HELM_API and HELM_TOKEN are set by helmstudio, or by helm dev.
helm = from_env()
def make_png(prompt, seed, size=256):
"""A gradient whose colours come from the prompt and the seed."""
digest = hashlib.sha256(("%s|%d" % (prompt, seed)).encode()).digest()
a, b = digest[0:3], digest[3:6]
rows = []
for y in range(size):
row = bytearray([0]) # no filter
for x in range(size):
t = (x + y) / (2 * (size - 1))
row += bytes(int(a[i] + (b[i] - a[i]) * t) for i in range(3))
rows.append(bytes(row))
def chunk(kind, data):
body = kind + data
return struct.pack(">I", len(data)) + body + struct.pack(">I", zlib.crc32(body) & 0xFFFFFFFF)
header = struct.pack(">IIBBBBB", size, size, 8, 2, 0, 0, 0)
return (b"\x89PNG\r\n\x1a\n" + chunk(b"IHDR", header)
+ chunk(b"IDAT", zlib.compress(b"".join(rows))) + chunk(b"IEND", b""))
def make(prompt, seed):
"""Write the image to the stage, adopt it, and record it."""
stage = os.environ["HELM_STAGE_DIR"]
path = os.path.join(stage, "hello-%d.png" % seed)
with open(path, "wb") as f:
f.write(make_png(prompt, seed))
# Adopting moves the file into helmstudio's asset store without copying it.
asset = helm.assets.adopt({"path": path, "kind": "image"})
# The item is what the gallery shows, with what made it.
item = helm.gallery.add({
"kind": "image",
"asset_id": asset["id"],
"title": prompt,
"params": {"prompt": prompt, "seed": seed, "width": 256, "height": 256},
})
return {"item_id": item["id"], "asset_id": asset["id"]}
class Handler(BaseHTTPRequestHandler):
def reply(self, status, body):
data = json.dumps(body).encode()
self.send_response(status)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(data)))
self.end_headers()
self.wfile.write(data)
def do_GET(self):
if self.path == "/healthz":
return self.reply(200, {"ok": True})
if self.path == "/items":
page = helm.gallery.query(limit=10)
return self.reply(200, [{"id": i["id"], "title": i.get("title"), "params": i.get("params")}
for i in page["items"]])
return self.reply(200, {"studio": "hello studio", "make": "POST /make {prompt, seed}", "items": "GET /items"})
def do_POST(self):
if self.path != "/make":
return self.reply(404, {"error": "not_found"})
length = int(self.headers.get("Content-Length") or 0)
body = json.loads(self.rfile.read(length) or b"{}")
return self.reply(200, make(body.get("prompt", "a lighthouse at dusk"), int(body.get("seed", 42))))
def log_message(self, format, *args):
pass # keep helm dev's output to what matters
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--port", type=int, required=True)
args = parser.parse_args()
print("hello studio: http://127.0.0.1:%d" % args.port, flush=True)
ThreadingHTTPServer(("127.0.0.1", args.port), Handler).serve_forever()
if __name__ == "__main__":
main()
6. Run it
helm dev keeps the studio's data in .helm, beside the manifest. It serves the platform API on a free local port, gives the studio a token for the two capabilities its manifest asks for, starts its process, and waits for its health check.
../helmstudio/bin/helm dev -f helmstudio.yaml
It prints the API's address and where the data is, then the studio's output, each line prefixed with its process's name — first the command it started, then what the studio itself printed:
helm dev: hello-studio is starting; studio API http://127.0.0.1:…/api/v1, data in …/hello-studio/.helm
studio | [helmstudio …: starting studio in …/hello-studio: python3 studio.py --port 8765]
studio | hello studio: http://127.0.0.1:8765
The studio asked for port 8765. If something else has it, helm dev gives the studio another, and the line above says which — use that one below. Leave it running, and use a second terminal for the rest. Ctrl-C stops it.
7. Make something
curl -s -X POST http://127.0.0.1:8765/make -d '{"prompt": "a lighthouse at dusk", "seed": 7}'
The answer names the gallery item and the asset it holds.
8. See it recorded
curl -s http://127.0.0.1:8765/items
The item comes back with the prompt and the seed that made it. That is provenance: wherever the item is shown later, what made it is shown with it.
9. Check it against the criteria
../helmstudio/bin/helm validate -criteria helmstudio.yaml
helm validate checks a manifest against the schema and the rules the schema cannot express. With -criteria it also scores the fifteen criteria a published studio is held to, as far as a manifest can answer them. The example fails two: it declares no test profile, because the harness that would run one is not built, and it pins no ref, because it has no repository yet. Publishing explains both.
Next
- The manifest, and the manifest reference.
- Record with provenance, for outputs made from other outputs.
- Develop in isolation, for a studio with weights and a Python version of its own.