The OpenAI SDK's legacy multipart path only accepts dall-e-2 when
raw bytes are passed. Wrapping in io.BytesIO with a name attribute
routes through the newer path that supports gpt-image-* models.
Also removes output_format from the edit call as that endpoint
does not support it.
- Add hokusai/image.py with Pillow-based format detection and conversion
- Pass output_format to OpenAI gpt-image models and BFL API
- Convert mismatched images after provider writes (fallback for all providers)
- Handle RGBA-to-RGB flattening for JPEG targets
- Add Pillow dependency
When a target is present in the state file but no longer in the config,
its output file is deleted (or archived if archive_folder is set) and
its state entry is removed. This runs at the start of every build.
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.
Content targets write a string directly to the output file without
invoking any AI provider. They don't require API keys and are not
archived when overwritten.
Example usage in .hokusai.yaml:
file.txt:
content: ABC
The regenerate command accepts one or more target names and forces them
to be rebuilt even if they are up to date. This respects the
archive_folder setting, archiving previous versions before overwriting.
Download targets now store only 'download: <url>' in the state file
instead of using 'prompt' and 'model: __download__' as a workaround.
Also use exclude_defaults=True when serializing state to omit empty
fields like input_hashes: {} and extra_params: {}.
When archive_folder is set in the project config, artifacts are moved to
numbered archive copies (e.g. x.01.jpg, x.02.jpg) instead of being
overwritten or deleted.
- Build command archives existing artifacts before rebuilding dirty targets
- Clean command moves files to archive instead of deleting them
- Subfolder structure is preserved in the archive directory
- State file is always deleted, never archived
Build targets with subfolder paths (e.g. img/file.jpg) failed because
parent directories did not exist. Create them on demand in
_build_single_target before dispatching to providers.
Also clean up empty subdirectories in the clean command after removing
target files.
gpt-image-* models return b64_json by default and reject the
response_format parameter with a 400 error. Only pass it for
DALL-E models which default to url.
Skip the file-existence check when the prompt contains newlines (can't
be a filename) and catch OSError for prompts that exceed the OS path
length limit.
- State filename now derives from config: cards.bulkgen.yaml produces
.cards.bulkgen-state.yaml instead of .bulkgen.state.yaml
- Store resolved prompt text and extra params directly in state file
instead of hashing them, making state files human-readable
- Only file input contents remain hashed (SHA-256)
- Thread project_name through builder and CLI
- Remove hash_string() and _extra_hash() helpers
- Update .gitignore pattern to .*.bulkgen-state.yaml
- Add openai_text.py: text generation via OpenAI chat completions API
(gpt-4o, gpt-4o-mini, gpt-4.1, gpt-4.1-mini, gpt-4.1-nano, o3-mini)
- Add openai_image.py: image generation via OpenAI images API
(gpt-image-1 with reference image support, dall-e-3, dall-e-2)
- Refactor builder provider dispatch from TargetType to model-name index
to support multiple providers per target type
- Fix circular import between config.py and providers/__init__.py
using TYPE_CHECKING guard
- Fix stale default model assertions in tests
- Add openai>=1.0.0 dependency
- Rename ImageProvider to BlackForestProvider, TextProvider to MistralProvider
- Add get_provided_models() abstract method to Provider base class
- Move model lists from models.py into each provider's get_provided_models()
- Add providers/registry.py to aggregate models from all providers
- Extract infer_required_capabilities and resolve_model from config.py to resolve.py
- Update tests to use new names and import paths
- Add click as explicit dependency (already bundled with typer)
- Replace typer.echo calls with click.echo + click.style for colorized output
- Add BuildEvent enum and ProgressCallback to builder for decoupled progress reporting
- Remove direct typer dependency from builder module
- Show per-target status with colored labels (skip/ok/fail/...)
- Display elapsed build time in summary
- Colorize graph and clean command output
- Update CLI tests to match new output format
The text provider now includes reference_images alongside inputs when
building prompts. Image files are sent as base64 data URLs via
ImageURLChunk for actual multimodal vision support, replacing the
previous [Attached image: ...] placeholder text.
Replace singular reference_image field with reference_images list to
support an arbitrary number of reference images. Map them to the correct
BFL API parameter names based on model family:
- flux-2-*: input_image, input_image_2, ..., input_image_8
- flux-kontext-*: input_image, input_image_2, ..., input_image_4
- flux 1.x: image_prompt (single)
BREAKING CHANGE: reference_image config field renamed to reference_images (list).
The tests.conftest import could not be resolved in the nix sandbox
because tests is not a proper package. Define the WriteConfig type
alias directly in test_builder.py and test_graph.py instead.
Implement bulkgen/providers/bfl.py with a fully async httpx-based client
that supports all current and future BFL models (including flux-2-*).
Remove the blackforest dependency and simplify the image provider by
eliminating the asyncio.to_thread wrapper.
Add cachix/git-hooks.nix input and wire basedpyright, ruff,
ruff-format, and commitizen hooks into flake checks and devShell.
The basedpyright hook runs against a Nix-built venv so imports
resolve correctly in the sandbox.
Expose homeManagerModules.bulkgen and homeManagerModules.default
from the flake. The module provides programs.bulkgen.enable and
programs.bulkgen.package options, adding bulkgen to home.packages
when enabled.