Skip to content
helmstudio
Contents

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 helm from 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

quickstart/01-clone.sh Not run by the gate: it needs the network; the gate runs every other step in the checkout it tests
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.

quickstart/02-build-helm.sh
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.

quickstart/03-environment.sh
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.

quickstart/04-install-sdk.sh Not run by the gate: it needs the network; the gate puts the same directory on the environment's path instead
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.

quickstart/05-copy-example.sh
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:

hello-studio/helmstudio.yaml
# 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/studio.py
"""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.

quickstart/06-run.sh
../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:

quickstart/06-output.txt
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

quickstart/07-make.sh
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

quickstart/08-items.sh
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

quickstart/09-validate.sh
../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