Content targets write literal text to files via 'content:' field, without requiring an AI provider or API keys. They are not archived when overwritten. Loop expansion allows defining 'loops:' at the top level with named lists of values. Targets with [var] in their name are expanded via cartesian product. Variables are substituted in all string fields. Explicit targets override expanded ones. Escaping: \[var] -> [var]. Expansion happens at config load time so the rest of the system (builder, graph, state) sees only expanded targets.
9.4 KiB
hokusai
A build tool for AI-generated artifacts. Define image and text targets in a YAML config, and hokusai handles dependency resolution, incremental builds, and parallel execution.
Uses Mistral and OpenAI for text generation, and BlackForestLabs (FLUX) and OpenAI for image generation.
The name Hokusai was chosen in honor of Katsushika Hokusai, who produced over 30,000 paintings, sketches, woodblock prints, and images for picture books, many in larger series.
Installation
Requires Python 3.13+.
pip install .
Or with uv:
uv sync
Quick start
- Set your API keys:
export MISTRAL_API_KEY="your-key"
export BFL_API_KEY="your-key"
export OPENAI_API_KEY="your-key"
- Create a config file (e.g.
my-project.hokusai.yaml):
defaults:
text_model: mistral-large-latest
image_model: flux-pro
targets:
hero.png:
prompt: "A dramatic sunset over mountains, photorealistic"
width: 1024
height: 768
blog-post.md:
prompt: prompts/write-blog.txt
inputs:
- hero.png
- notes.md
- Build:
hokusai build
Config format
The config file must be named <anything>.hokusai.yaml and placed in your project directory. One config file per directory.
Top-level fields
| Field | Description |
|---|---|
defaults |
Default model names (optional) |
loops |
Loop variables for target template expansion (optional) |
archive_folder |
Directory to move previous outputs into before rebuilding (optional) |
targets |
Map of output filenames to their configuration |
Defaults
defaults:
text_model: mistral-large-latest # used for .md, .txt targets
image_model: flux-pro # used for .png, .jpg, .jpeg, .webp targets
Target fields
| Field | Type | Description |
|---|---|---|
prompt |
string | Inline prompt text, or path to a prompt file |
model |
string | Override the default model for this target |
inputs |
list[string] | Files this target depends on (other targets or existing files) |
reference_images |
list[string] | Image files for image-to-image generation |
control_images |
list[string] | Control images (for canny/depth models) |
width |
int | Image width in pixels |
height |
int | Image height in pixels |
download |
string | URL to download instead of generating (mutually exclusive with prompt) |
content |
string | Literal text to write to the file (mutually exclusive with prompt/download) |
Target type is inferred from the file extension:
- Image:
.png,.jpg,.jpeg,.webp - Text:
.md,.txt
Prompts
Prompts can be inline strings or file references:
targets:
# Inline prompt
image.png:
prompt: "A cat sitting on a windowsill"
# File reference (reads the file contents as the prompt)
article.md:
prompt: prompts/article-prompt.txt
If the prompt value is a path to an existing file, its contents are read. Otherwise the string is used directly.
Dependencies
Targets can depend on other targets or on existing files in the project directory:
targets:
base.png:
prompt: "A landscape scene"
variant.png:
prompt: "Same scene but in winter"
reference_image: base.png # image-to-image, depends on base.png
summary.md:
prompt: "Summarize these notes"
inputs:
- base.png # depends on a generated target
- research-notes.md # depends on an existing file
hokusai resolves dependencies automatically. If you build a single target, its transitive dependencies are included.
Download targets
Targets can download files from URLs instead of generating them:
targets:
reference.jpg:
download: https://example.com/image.jpg
variation.png:
prompt: "A variation of this image in watercolor style"
reference_images:
- reference.jpg
Download targets participate in dependency resolution like any other target. They are skipped if the URL hasn't changed.
Content targets
Targets can write literal text content directly to a file without invoking any AI provider:
targets:
config.txt:
content: "Some static configuration"
data.csv:
content: |
name,value
alpha,1
beta,2
Content targets don't require API keys and are not archived when overwritten. They participate in dependency resolution like any other target, so generated targets can depend on them.
Loops
Define loops at the top level to generate multiple targets from a template using cartesian products:
loops:
color:
- red
- blue
- green
size:
- small
- large
targets:
card-[color]-[size].png:
prompt: "A [color] card in [size] format"
width: 1024
height: 768
This expands to 6 targets: card-red-small.png, card-red-large.png, card-blue-small.png, etc. Loop variables are substituted in all string fields: prompts, inputs, reference images, control images, download URLs, and content.
Only variables that appear in the target name cause expansion. A target without any [var] references in its name is not looped:
loops:
id:
- 1
- 2
targets:
data-[id].txt:
content: "Data for [id]"
# This target depends on ALL expanded data files
summary.md:
prompt: "Summarize everything"
inputs:
- data-1.txt
- data-2.txt
Loop variables also work across dependent targets:
targets:
data-[id].txt:
content: "Data for item [id]"
report-[id].md:
prompt: "Write a report about item [id]"
inputs:
- data-[id].txt
Explicit overrides: If you define both a template and an explicit target that would collide, the explicit target wins:
targets:
image-[n].png:
prompt: "Generic image [n]"
image-3.png:
prompt: "Special custom image" # this overrides the template for n=3
Escaping: Use \[var] to produce a literal [var] in the output.
Loop values are always treated as strings. Numbers and booleans in YAML are automatically converted.
Archiving previous outputs
Set archive_folder at the top level to preserve previous versions of generated files. When a target is rebuilt, the existing output is moved to the archive folder with an incrementing numeric suffix:
archive_folder: archive
targets:
hero.png:
prompt: "A dramatic sunset over mountains"
On each rebuild of hero.png, the previous file is archived as archive/hero.01.png, archive/hero.02.png, etc. The archive directory is created automatically if it doesn't exist.
CLI
hokusai build [target]
Build all targets, or a specific target and its dependencies.
- Skips targets that are already up to date (incremental builds)
- Runs independent targets in parallel
- Continues building if a target fails (dependents of the failed target are skipped)
hokusai regenerate <targets...>
Force regeneration of specific targets, ignoring their up-to-date status. Useful for getting a new variation of an AI-generated output without changing the prompt.
hokusai regenerate hero.png # regenerate one target
hokusai regenerate hero.png logo.png # regenerate multiple targets
If archive_folder is set, the previous versions are archived before regeneration.
hokusai clean
Remove all generated target files and the build state file (.hokusai.state.yaml). Input files are preserved.
If archive_folder is set, files are moved to the archive instead of being deleted.
hokusai graph
Print the dependency graph showing build stages:
Stage 0 (inputs): research-notes.md
Stage 0 (targets): base.png
Stage 1 (targets): variant.png, summary.md
variant.png <- base.png
summary.md <- base.png, research-notes.md
Incremental builds
hokusai tracks the state of each build in .hokusai.state.yaml (auto-generated, add to .gitignore). A target is rebuilt when any of these change:
- Input file contents (SHA-256 hash)
- Prompt text
- Model name
- Extra parameters (width, height, etc.)
Installation with Nix / home-manager
hokusai provides a Nix flake with a home-manager module. Add the flake as an input and enable the module:
# flake.nix
{
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
home-manager.url = "github:nix-community/home-manager";
hokusai.url = "github:kfickel/hokusai"; # adjust to your actual repo URL
};
outputs = { nixpkgs, home-manager, hokusai, ... }: {
# ... your existing config, then in homeConfigurations:
homeConfigurations."user" = home-manager.lib.homeManagerConfiguration {
# ...
modules = [
hokusai.homeManagerModules.hokusai
{
programs.hokusai.enable = true;
}
];
};
};
}
This places the hokusai binary on your $PATH. To use a different package build (e.g. from a different system or overlay), set programs.hokusai.package.
The flake also exposes:
packages.<system>.hokusai— the standalone package, usable without home-manager (e.g.nix run github:kfickel/hokusai)devShells.<system>.default— development shell with all dependencies
Environment variables
| Variable | Required for |
|---|---|
MISTRAL_API_KEY |
Text targets via Mistral models |
BFL_API_KEY |
Image targets via FLUX models |
OPENAI_API_KEY |
Text and image targets via OpenAI models |
