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.
347 lines
9.4 KiB
Markdown
347 lines
9.4 KiB
Markdown
# 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](https://mistral.ai) and [OpenAI](https://openai.com) for text generation, and [BlackForestLabs](https://blackforestlabs.ai) (FLUX) and [OpenAI](https://openai.com) for image generation.
|
|
|
|
The name Hokusai was chosen in honor of [Katsushika Hokusai](https://en.wikipedia.org/wiki/Hokusai), who produced over 30,000 paintings, sketches, woodblock prints, and images for picture books, many in larger series.
|
|
|
|
## Installation
|
|
|
|
Requires Python 3.13+.
|
|
|
|
```bash
|
|
pip install .
|
|
```
|
|
|
|
Or with [uv](https://docs.astral.sh/uv/):
|
|
|
|
```bash
|
|
uv sync
|
|
```
|
|
|
|
## Quick start
|
|
|
|
1. Set your API keys:
|
|
|
|
```bash
|
|
export MISTRAL_API_KEY="your-key"
|
|
export BFL_API_KEY="your-key"
|
|
export OPENAI_API_KEY="your-key"
|
|
```
|
|
|
|
2. Create a config file (e.g. `my-project.hokusai.yaml`):
|
|
|
|
```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
|
|
```
|
|
|
|
3. Build:
|
|
|
|
```bash
|
|
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
|
|
|
|
```yaml
|
|
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:
|
|
|
|
```yaml
|
|
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:
|
|
|
|
```yaml
|
|
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:
|
|
|
|
```yaml
|
|
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:
|
|
|
|
```yaml
|
|
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:
|
|
|
|
```yaml
|
|
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:
|
|
|
|
```yaml
|
|
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:
|
|
|
|
```yaml
|
|
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:
|
|
|
|
```yaml
|
|
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:
|
|
|
|
```yaml
|
|
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.
|
|
|
|
```bash
|
|
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:
|
|
|
|
```nix
|
|
# 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 |
|