Copyright 2025-2026 Ardan Labs
This project lets you use Go for hardware accelerated local image and video generation with stable-diffusion.cpp directly integrated into your applications. Malina maps the safe public stable-diffusion.h API plus pure-Go PNG/JPEG I/O and Motion-JPEG AVI muxing.
Malina is the image-generation sibling of ardanlabs/bucky (which binds whisper.cpp) and hybridgroup/yzma (which binds llama.cpp). The end goal is to give Kronk a native, OpenAI-compatible POST /v1/images/generations endpoint without the CGo toolchain.
Malina is the Russian word for "raspberry" — a small, dense, fast-growing fruit. Naming a stable-diffusion binding after a fast little thing that sprouts colorful pictures is just good taste.
To install malina, fetch the stable-diffusion.cpp shared libraries, and generate the bundled cat sample:
$ go install github.com/ardanlabs/malina@latest
$ malina install -u
$ malina model pull sd-1.5
$ go run ./examples/hello "a lovely cat"Sometimes there are breaking changes to stable-diffusion.cpp that require an update to malina. Here are the known compatible versions:
| stable-diffusion.cpp | malina |
|---|---|
| master-929-3f8527a | 1.1.4 |
| master-908-88411ef | 1.1.3 |
| master-869-07a85c7 | 1.1.2 |
The FFI binding includes image and native video generation, upscaling, ADetailer, ControlNet hot-swap, conversion, Canny preprocessing, cancellation, preview/backend callbacks, device and loaded-model identification, and every generation parameter in the target header. Pure-Go PNG/JPEG decode + Motion-JPEG AVI mux, the CLI (install, system, info, model list|pull), and runnable examples for the generation APIs have also landed. Kronk integration (an OpenAI-compatible POST /v1/images/generations endpoint) lives in the kronk repo.
Name: Bill Kennedy
Company: Ardan Labs
Title: Managing Partner
Email: bill@ardanlabs.com
BlueSky: https://bsky.app/profile/goinggo.net
LinkedIn: www.linkedin.com/in/william-kennedy-5b318778/
Twitter: https://x.com/goinggodotnet
The fastest way to install on any supported platform is with Go:
$ go install github.com/ardanlabs/malina@latest
$ malina --helpThen fetch the stable-diffusion.cpp shared library bundle (dylib on macOS, DLLs on Windows, and .so files on Linux, all distributed in ZIP archives from the upstream leejet/stable-diffusion.cpp releases):
$ malina install
$ malina systemMalina verifies an upstream asset's GitHub SHA-256 digest before extracting
it. DefaultSDVersion includes the SHA-256 of the embedded trusted manifest,
which authenticates the asset ID, size, archive digest, and every installed
shared library for Malina's pinned stable-diffusion.cpp release. Installs
created before this verification metadata was introduced must be refreshed
once with malina install --upgrade.
And pull a model bundle from the bundled catalog:
$ malina model list
$ malina model pull sd-1.5
$ malina model info -m ~/.kronk/malina-models/sd-1.5/v1-5-pruned-emaonly.safetensorsHere is the existing Issues/Features for the project and the things being worked on or things that would be nice to have.
If you are interested in helping in any way, please send an email to Bill Kennedy.
The architecture of malina mirrors bucky and yzma file-for-file so anyone who knows either can drop straight in. There is no CGo: every C call goes through purego + JupiterRider/ffi.
┌─────────────────────────────────────────────────────────────┐
│ cmd/ malina CLI (install, system, model, sd) │
├─────────────────────────────────────────────────────────────┤
│ pkg/sd safe stable-diffusion.h FFI surface │
│ (image/video generation, upscaler, callbacks, │
│ conversion, image I/O, log, system) │
│ pkg/download go-getter-driven release-archive resolver + │
│ curated generation and tool-model catalog │
│ pkg/loader MALINA_LIB-aware purego library loader │
│ pkg/utils cross-platform Go ↔ C string helpers │
└─────────────────────────────────────────────────────────────┘
│
▼
libstable-diffusion.{dylib|so|dll}
(stable-diffusion.cpp master-929)
pkg/sd prepares 61 of the 64 functions exported by the pinned
stable-diffusion.h. This includes all functions with a safe ownership
contract. Newer optional symbols are resolved at load time so an explicitly
requested older compatible library can still load; calling an unavailable
feature returns sd.ErrUnsupportedAPI.
The only functions intentionally not called are sd_ctx_params_to_str,
sd_sample_params_to_str, and sd_img_gen_params_to_str. Each returns a
newly allocated char *, but upstream provides no matching public deallocator.
Freeing those pointers from Go would be unsafe across Windows CRT boundaries,
and not freeing them would leak. Malina will bind them when upstream exposes a
matched free API. The exported sample_method_to_str and scheduler_to_str
data arrays are represented by the safe name and parse functions instead of
directly exposing C global memory.
The master-846-d8fb10c ABI replaces ContextParams.StreamLayers with
ContextParams.DisablePrefetch, adds ContextParams.DisableSegmentedCompute,
and inserts the LogVerbose level. Code setting StreamLayers must migrate to
the new controls; the old field's storage now has the opposite meaning.
The master-869-07a85c7 ABI adds ContextParams.AudioEncoderPath for
audio-conditioned video models and ContextParams.Tokenizer for external
tokenizer JSON files. It also adds an output parameter to native video
generation, so older Malina releases must not load this library build. Use
GenerateVideoWithFPS when muxing output so models that force a fixed frame
rate, such as Wan2.2 S2V and MiniMax-H3, report the effective value.
The master-908-88411ef ABI grows the context, image-generation, and
video-generation parameter structs. Malina 1.1.3 exposes native SageAttention,
the per-context conditioning-cache limit, LLaDA-Image's scheduler, and shared
image-input preprocessing rules through ContextParams, ImgGenParams, and
VideoGenParams. Older Malina releases must not load this library build.
The master-929-3f8527a header keeps every struct size, field offset, enum,
callback, and existing function signature unchanged. It renames VAE tiling's
X/Y fields to width/height and defines tile dimensions in image pixels; Malina
keeps the source-compatible TileSizeX/TileSizeY and
RelativeSizeX/RelativeSizeY names while documenting those semantics.
Malina 1.1.4 also exposes GetUpscalerModelScale, which reads an ESRGAN
model's native scale without first loading an upscaler context.
Malina works with any model stable-diffusion.cpp accepts: .safetensors and .gguf checkpoints for SD 1.x / SD 2.x / SDXL, plus the multi-file FLUX and SD3 layouts (separate diffusion model + VAE + text-encoder files). Recommended hosts are stable-diffusion-v1-5/stable-diffusion-v1-5 and the GGUF quants under city96.
The target library also supports SenseNova U1.5 directories. It is not in the curated catalog because upstream requires a complete repository directory with eight weight shards plus tokenizer and configuration files, while catalog roles currently resolve to individual files.
The target library adds Wan2.2 S2V 14B audio-conditioned video support and requires external tokenizer JSON files for PiD and Lens. These are not curated bundles: Wan S2V needs a large multi-file video/audio workflow that the current catalog roles cannot represent, PiD's official weights are non-commercial, and Lens requires multiple large model components plus an external tokenizer.
The master-908-88411ef release also supports Qwen Image 2.1 and LLaDA-Image.
Qwen Image 2.1 is not curated because its license restricts use to
non-commercial research and evaluation. The catalog includes the Apache-2.0
LLaDA-Image-Turbo model for 4-step text-to-image generation and image editing.
The master-929-3f8527a release adds PixArt-α/Σ and Ming-Image Design.
Neither is curated yet: the official PixArt distribution is a multi-file
transformer/text-encoder/VAE layout and upstream does not yet apply PixArt-α's
resolution micro-conditioning, while Ming-Image requires a 6B diffusion model,
a BF16 Ling-mini-2.0 text encoder, a VAE, and an external tokenizer. Those
official workflows are impractically large for the curated catalog without
stable compact variants, though callers can supply their files directly.
Malina ships a curated catalog so you can pull complete generation workflows and standalone tool models instead of pasting URLs:
$ malina model list
$ malina model pull sd-1.5
$ malina model pull controlnet-canny-sd1.5
$ malina model pull realesrgan-x4-anime
$ malina model pull adetailer-face-yolov8n
$ malina model pull animatediff-sd1.5
$ malina model pull sdxl-base-1.0
$ malina model pull llada-image-turbo # five files; approximately 20.2 GB
$ malina model pull flux2-klein-4b # license-gated; export HF_TOKEN first
$ malina model pull flux2-klein-9b # license-gated; export HF_TOKEN firstEach bundle drops every required file into $HOME/.kronk/malina-models/<bundle>/ along with a manifest.json the examples use to resolve paths.
Malina uses the prebuilt stable-diffusion.cpp release artifacts from leejet/stable-diffusion.cpp directly — there is no companion builder repo. The pinned version is captured in pkg/download/install.go as DefaultSDVersion; its trusted release metadata, archive hashes, installed-file hashes, and symlink targets are captured in pkg/download/library_manifest.json. A dynamically selected release such as -v latest still receives archive-level verification from GitHub. Malina saves that GitHub Release API response and the resulting extracted-file hashes beside the installed libraries, so later offline checks can detect changed metadata or local corruption. Only the pinned release has an authenticated post-install baseline embedded in the Malina binary.
| OS | CPU | Backend | Upstream artifact pattern |
|---|---|---|---|
| macOS | arm64 | Metal | sd-master-…-bin-Darwin-macOS-…-arm64.zip |
| Windows | amd64 | CPU | sd-master-…-bin-win-cpu-x64.zip |
| Windows | amd64 | CUDA 12 | sd-master-…-bin-win-cuda12-x64.zip plus cudart-…-cu12-…zip |
| Windows | amd64 | Vulkan | sd-master-…-bin-win-vulkan-x64.zip |
| Windows | amd64 | ROCm | sd-master-…-bin-win-rocm-…-x64.zip |
| Linux | amd64 | CPU | sd-master-…-bin-Linux-Ubuntu-…-x86_64.zip |
| Linux | amd64 | Vulkan / ROCm | CPU pattern plus -vulkan.zip or -rocm-….zip |
Whenever there is a new release of stable-diffusion.cpp, the FFI struct mirrors in pkg/sd and the version constant in pkg/download may need a refresh. Generate and review the new trusted manifest with make generate-library-manifest VERSION=master-N-shortsha, bump DefaultSDVersion, regenerate any struct-size assertions in pkg/sd/*_test.go, and let CI verify. Manifest generation downloads and hashes every supported release asset, so it can take several minutes and several gigabytes of transfer.
The malina_model_tests suite exercises SD 1.5, SDXL, and the advanced APIs
against real models configured by the Makefile. The license-gated FLUX.2 Klein
bundles and the approximately 20.2 GB LLaDA-Image-Turbo bundle remain available
as opt-in catalog entries, but are deliberately not used by tests, benchmarks,
examples, or make download-models.
| Environment variable | Catalog bundle / functional test |
|---|---|
MALINA_CONTROLNET_TEST_DIR |
controlnet-canny-sd1.5 controlled image generation |
MALINA_UPSCALER_TEST_DIR |
realesrgan-x4-anime 4× image upscaling |
MALINA_ADETAILER_TEST_DIR |
adetailer-face-yolov8n face detection/refinement |
MALINA_VIDEO_TEST_DIR |
animatediff-sd1.5 multi-frame video generation |
Each advanced test skips when its fixture variable is unset and fails when a configured fixture is missing. GitHub Actions downloads and caches each advanced bundle before running its corresponding native functional test on Linux.
There are examples in the examples/ directory. They always load
libraries and models from Malina's default locations under ~/.kronk; no
library or model path configuration is required or accepted. Before loading,
each example verifies that the installed libraries exactly match the
authenticated download.DefaultSDVersion pin:
$ malina install -u
$ malina model pull sd-1.5SYSTEM — the smallest possible malina program: load libstable-diffusion and print the library version, system info, and GGML backend device count. No model required.
$ make example-systemHELLO — load a stable-diffusion model, generate one image from a text prompt, and save it as hello.png.
$ make example-helloCONCURRENT — compare serial generation with concurrent generation on independent native contexts. Contexts cannot be shared concurrently, and each independent context loads another copy of the model weights.
$ make example-concurrentIMG2IMG — image-to-image: load a source PNG or JPEG, hand it to stable-diffusion as the starting latent, and let the prompt repaint it. The default chain consumes hello.png written by the previous example.
$ make example-hello # writes hello.png
$ make example-img2img # writes img2img.png in oil-painting styleCONTROLNET — derive Canny edges from an input image and use them to constrain the generated image's composition.
$ malina model pull controlnet-canny-sd1.5
$ make example-controlnetUPSCALE — enlarge an image 4× with the compact Real-ESRGAN anime model.
$ malina model pull realesrgan-x4-anime
$ make example-upscaleADETAILER — detect faces with YOLOv8n and refine each detected region with Stable Diffusion inpainting.
$ malina model pull adetailer-face-yolov8n
$ make example-adetailerANIMATEDIFF — generate a temporally conditioned sequence with an AnimateDiff motion module and save it as an AVI.
$ malina model pull animatediff-sd1.5
$ make example-animatediffSD-ENCODE — mux a directory of PNG / JPEG frames into a Motion-JPEG AVI. No model is loaded; this is the pure-Go encoder built on top of pkg/sd's SaveAVI helper.
$ make example-sd-encode// hello is the smallest possible malina example: load a stable-diffusion
// model, generate one image from a text prompt, and save it as PNG.
package main
import (
"context"
"fmt"
"log"
"os"
"path/filepath"
"time"
"github.com/ardanlabs/malina/pkg/download"
"github.com/ardanlabs/malina/pkg/sd"
)
func main() {
prompt := "a lovely cat"
if len(os.Args) >= 2 {
prompt = os.Args[1]
}
libPath := download.DefaultLibrariesDir()
bundleDir := filepath.Join(download.DefaultModelsDir(), "sd-1.5")
manifest, err := download.LoadManifest(bundleDir)
if err != nil {
log.Fatalf("load model bundle from %s: %v (did you run `malina model pull sd-1.5`?)", bundleDir, err)
}
modelPath := manifest.Files[string(download.RoleModel)]
if err := download.VerifyDefaultInstall(context.Background(), libPath); err != nil {
log.Fatalf("verify default libraries: %v (did you run `malina install -u`?)", err)
}
if err := sd.Load(libPath); err != nil {
log.Fatalf("sd.Load: %v", err)
}
if err := sd.Init(libPath); err != nil {
log.Fatalf("sd.Init: %v", err)
}
cparams := sd.ContextParamsInit()
cparams.ModelPath = modelPath
fmt.Println("loading model from", modelPath, "...")
ctx, err := sd.NewContext(cparams)
if err != nil {
log.Fatalf("sd.NewContext: %v", err)
}
defer sd.FreeContext(ctx)
params := sd.ImgGenParamsInit()
params.Prompt = prompt
fmt.Println("generating image for prompt:", prompt)
start := time.Now()
img, err := sd.GenerateImage(ctx, params)
if err != nil {
log.Fatalf("sd.GenerateImage: %v", err)
}
elapsed := time.Since(start)
const outPath = "hello.png"
if err := img.SavePNG(outPath); err != nil {
log.Fatalf("SavePNG: %v", err)
}
fmt.Printf("wrote %s (%dx%d, %d channels) in %s\n", outPath, img.Width, img.Height, img.Channel, elapsed.Round(time.Millisecond))
}This example produces the following output:
$ make example-hello
go run ./examples/hello "a lovely cat"
loading model from /Users/bill/.kronk/malina-models/sd-1.5/v1-5-pruned-emaonly.safetensors ...
generating image for prompt: a lovely cat
wrote hello.png (512x512, 3 channels) in 6.842sApache-2.0 — see LICENSE.