hokusai/README.md
Konstantin Fickel 7503672942
All checks were successful
Continuous Integration / Build Package (push) Successful in 25s
Continuous Integration / Lint, Check & Test (push) Successful in 44s
feat: add content targets and loop expansion for target templates
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.
2026-02-21 18:39:13 +01:00

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

  1. Set your API keys:
export MISTRAL_API_KEY="your-key"
export BFL_API_KEY="your-key"
export OPENAI_API_KEY="your-key"
  1. 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
  1. 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