# hokusai ![](./logo.png) 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 `.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 ` 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..hokusai` — the standalone package, usable without home-manager (e.g. `nix run github:kfickel/hokusai`) - `devShells..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 |