chore: rename bulkgen to hokusai
This commit is contained in:
parent
a28cc97aed
commit
4def49350e
32 changed files with 215 additions and 213 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -14,8 +14,8 @@ wheels/
|
|||
.devenv.flake.nix
|
||||
.pre-commit-config.yaml
|
||||
|
||||
# bulkgen state
|
||||
.*.bulkgen-state.yaml
|
||||
# hokusai state
|
||||
.*.hokusai-state.yaml
|
||||
|
||||
# Nix
|
||||
result
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
3.14
|
||||
3.13
|
||||
|
|
|
|||
24
CLAUDE.md
24
CLAUDE.md
|
|
@ -1,17 +1,17 @@
|
|||
# CLAUDE.md - bulkgen development guide
|
||||
# CLAUDE.md - hokusai development guide
|
||||
|
||||
## Project overview
|
||||
|
||||
bulkgen is a `make`-like build tool for AI-generated artifacts (images and text). A YAML config file defines targets with dependencies; bulkgen builds a DAG with networkx and executes generation in parallel topological order using Mistral (text) and BlackForestLabs (images) as providers.
|
||||
hokusai is a `make`-like build tool for AI-generated artifacts (images and text). A YAML config file defines targets with dependencies; hokusai builds a DAG with networkx and executes generation in parallel topological order using Mistral (text) and BlackForestLabs (images) as providers.
|
||||
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
uv sync # install dependencies
|
||||
uv run bulkgen build # build all targets
|
||||
uv run bulkgen build X # build target X and its transitive deps
|
||||
uv run bulkgen clean # remove generated artifacts + state file
|
||||
uv run bulkgen graph # print dependency graph with stages
|
||||
uv run hokusai build # build all targets
|
||||
uv run hokusai build X # build target X and its transitive deps
|
||||
uv run hokusai clean # remove generated artifacts + state file
|
||||
uv run hokusai graph # print dependency graph with stages
|
||||
uv run pytest # run tests
|
||||
```
|
||||
|
||||
|
|
@ -45,14 +45,14 @@ ruff format --check
|
|||
### Module structure
|
||||
|
||||
```
|
||||
main.py # Entry point: imports and runs bulkgen.cli.app
|
||||
bulkgen/
|
||||
main.py # Entry point: imports and runs hokusai.cli.app
|
||||
hokusai/
|
||||
__init__.py
|
||||
cli.py # Typer CLI: build, clean, graph commands
|
||||
config.py # Pydantic models for YAML config
|
||||
graph.py # networkx DAG construction and traversal
|
||||
builder.py # Build orchestrator: incremental + parallel
|
||||
state.py # .bulkgen.state.yaml hash tracking
|
||||
state.py # .hokusai.state.yaml hash tracking
|
||||
providers/
|
||||
__init__.py # Abstract Provider base class (ABC)
|
||||
image.py # BlackForestLabs image generation
|
||||
|
|
@ -61,7 +61,7 @@ bulkgen/
|
|||
|
||||
### Data flow
|
||||
|
||||
1. **cli.py** finds the `*.bulkgen.yaml` in cwd, calls `load_config()` from `config.py`
|
||||
1. **cli.py** finds the `*.hokusai.yaml` in cwd, calls `load_config()` from `config.py`
|
||||
2. **config.py** parses YAML into `ProjectConfig` (pydantic), which contains `Defaults` and `dict[str, TargetConfig]`
|
||||
3. **graph.py** builds an `nx.DiGraph` from target dependencies. `get_build_order()` uses `nx.topological_generations()` to return parallel batches
|
||||
4. **builder.py** `run_build()` iterates generations. Per generation:
|
||||
|
|
@ -77,13 +77,13 @@ bulkgen/
|
|||
- **Prompt resolution**: if the `prompt` string is a path to an existing file, its contents are read; otherwise it's used as-is. Done in `builder.py:_resolve_prompt()`.
|
||||
- **BFL client is synchronous**: wrapped in `asyncio.to_thread()` in `providers/image.py`. Uses `ClientConfig(sync=True, timeout=300)` for internal polling.
|
||||
- **Mistral client is natively async**: uses `complete_async()` directly in `providers/text.py`.
|
||||
- **Incremental builds**: `.bulkgen.state.yaml` tracks per-target: input file hashes, prompt hash, model name, and extra params hash. Any change marks the target dirty.
|
||||
- **Incremental builds**: `.hokusai.state.yaml` tracks per-target: input file hashes, prompt hash, model name, and extra params hash. Any change marks the target dirty.
|
||||
- **Error isolation**: if a target fails, its dependents are marked "Dependency failed" but independent targets continue building.
|
||||
- **State saved per-generation**: partial progress survives crashes. At most one generation of work is lost.
|
||||
|
||||
### Provider interface
|
||||
|
||||
All providers implement `bulkgen.providers.Provider`:
|
||||
All providers implement `hokusai.providers.Provider`:
|
||||
```python
|
||||
async def generate(self, target_name, target_config, resolved_prompt, resolved_model, project_dir) -> None
|
||||
```
|
||||
|
|
|
|||
38
README.md
38
README.md
|
|
@ -1,9 +1,11 @@
|
|||
# bulkgen
|
||||
# hokusai
|
||||
|
||||
A build tool for AI-generated artifacts. Define image and text targets in a YAML config, and bulkgen handles dependency resolution, incremental builds, and parallel execution.
|
||||
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+.
|
||||
|
|
@ -28,7 +30,7 @@ export BFL_API_KEY="your-key"
|
|||
export OPENAI_API_KEY="your-key"
|
||||
```
|
||||
|
||||
2. Create a config file (e.g. `my-project.bulkgen.yaml`):
|
||||
2. Create a config file (e.g. `my-project.hokusai.yaml`):
|
||||
|
||||
```yaml
|
||||
defaults:
|
||||
|
|
@ -51,12 +53,12 @@ targets:
|
|||
3. Build:
|
||||
|
||||
```bash
|
||||
bulkgen build
|
||||
hokusai build
|
||||
```
|
||||
|
||||
## Config format
|
||||
|
||||
The config file must be named `<anything>.bulkgen.yaml` and placed in your project directory. One config file per directory.
|
||||
The config file must be named `<anything>.hokusai.yaml` and placed in your project directory. One config file per directory.
|
||||
|
||||
### Top-level fields
|
||||
|
||||
|
|
@ -127,7 +129,7 @@ targets:
|
|||
- research-notes.md # depends on an existing file
|
||||
```
|
||||
|
||||
bulkgen resolves dependencies automatically. If you build a single target, its transitive dependencies are included.
|
||||
hokusai resolves dependencies automatically. If you build a single target, its transitive dependencies are included.
|
||||
|
||||
### Archiving previous outputs
|
||||
|
||||
|
|
@ -145,7 +147,7 @@ On each rebuild of `hero.png`, the previous file is archived as `archive/hero.01
|
|||
|
||||
## CLI
|
||||
|
||||
### `bulkgen build [target]`
|
||||
### `hokusai build [target]`
|
||||
|
||||
Build all targets, or a specific target and its dependencies.
|
||||
|
||||
|
|
@ -153,11 +155,11 @@ Build all targets, or a specific target and its dependencies.
|
|||
- Runs independent targets in parallel
|
||||
- Continues building if a target fails (dependents of the failed target are skipped)
|
||||
|
||||
### `bulkgen clean`
|
||||
### `hokusai clean`
|
||||
|
||||
Remove all generated target files and the build state file (`.bulkgen.state.yaml`). Input files are preserved.
|
||||
Remove all generated target files and the build state file (`.hokusai.state.yaml`). Input files are preserved.
|
||||
|
||||
### `bulkgen graph`
|
||||
### `hokusai graph`
|
||||
|
||||
Print the dependency graph showing build stages:
|
||||
|
||||
|
|
@ -171,7 +173,7 @@ Stage 1 (targets): variant.png, summary.md
|
|||
|
||||
## Incremental builds
|
||||
|
||||
bulkgen tracks the state of each build in `.bulkgen.state.yaml` (auto-generated, add to `.gitignore`). A target is rebuilt when any of these change:
|
||||
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
|
||||
|
|
@ -180,7 +182,7 @@ bulkgen tracks the state of each build in `.bulkgen.state.yaml` (auto-generated,
|
|||
|
||||
## Installation with Nix / home-manager
|
||||
|
||||
bulkgen provides a Nix flake with a home-manager module. Add the flake as an input and enable the module:
|
||||
hokusai provides a Nix flake with a home-manager module. Add the flake as an input and enable the module:
|
||||
|
||||
```nix
|
||||
# flake.nix
|
||||
|
|
@ -188,17 +190,17 @@ bulkgen provides a Nix flake with a home-manager module. Add the flake as an inp
|
|||
inputs = {
|
||||
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
|
||||
home-manager.url = "github:nix-community/home-manager";
|
||||
bulkgen.url = "github:kfickel/bulkgen"; # adjust to your actual repo URL
|
||||
hokusai.url = "github:kfickel/hokusai"; # adjust to your actual repo URL
|
||||
};
|
||||
|
||||
outputs = { nixpkgs, home-manager, bulkgen, ... }: {
|
||||
outputs = { nixpkgs, home-manager, hokusai, ... }: {
|
||||
# ... your existing config, then in homeConfigurations:
|
||||
homeConfigurations."user" = home-manager.lib.homeManagerConfiguration {
|
||||
# ...
|
||||
modules = [
|
||||
bulkgen.homeManagerModules.bulkgen
|
||||
hokusai.homeManagerModules.hokusai
|
||||
{
|
||||
programs.bulkgen.enable = true;
|
||||
programs.hokusai.enable = true;
|
||||
}
|
||||
];
|
||||
};
|
||||
|
|
@ -206,11 +208,11 @@ bulkgen provides a Nix flake with a home-manager module. Add the flake as an inp
|
|||
}
|
||||
```
|
||||
|
||||
This places the `bulkgen` binary on your `$PATH`. To use a different package build (e.g. from a different system or overlay), set `programs.bulkgen.package`.
|
||||
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>.bulkgen` — the standalone package, usable without home-manager (e.g. `nix run github:kfickel/bulkgen`)
|
||||
- `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
|
||||
|
|
|
|||
30
flake.nix
30
flake.nix
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
description = "bulkgen - Bulk-Generate Images with Generative AI";
|
||||
description = "hokusai - Bulk-Generate Images with Generative AI";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
|
||||
|
|
@ -63,15 +63,15 @@
|
|||
};
|
||||
|
||||
pyprojectOverrides = _final: prev: {
|
||||
bulkgen = prev.bulkgen.overrideAttrs (old: {
|
||||
hokusai = prev.hokusai.overrideAttrs (old: {
|
||||
passthru = old.passthru // {
|
||||
tests = (old.passthru.tests or { }) // {
|
||||
pytest = stdenv.mkDerivation {
|
||||
name = "${_final.bulkgen.name}-pytest";
|
||||
inherit (_final.bulkgen) src;
|
||||
name = "${_final.hokusai.name}-pytest";
|
||||
inherit (_final.hokusai) src;
|
||||
nativeBuildInputs = [
|
||||
(_final.mkVirtualEnv "bulkgen-pytest-env" {
|
||||
bulkgen = [ "dev" ];
|
||||
(_final.mkVirtualEnv "hokusai-pytest-env" {
|
||||
hokusai = [ "dev" ];
|
||||
})
|
||||
];
|
||||
dontConfigure = true;
|
||||
|
|
@ -108,7 +108,7 @@
|
|||
let
|
||||
pkgs = nixpkgs.legacyPackages.${system};
|
||||
pythonSet = pythonSets.${system};
|
||||
venv = pythonSet.mkVirtualEnv "bulkgen-check-env" workspace.deps.default;
|
||||
venv = pythonSet.mkVirtualEnv "hokusai-check-env" workspace.deps.default;
|
||||
in
|
||||
git-hooks.lib.${system}.run {
|
||||
src = ./.;
|
||||
|
|
@ -142,17 +142,17 @@
|
|||
inherit (pkgs.callPackages pyproject-nix.build.util { }) mkApplication;
|
||||
in
|
||||
rec {
|
||||
bulkgen = mkApplication {
|
||||
venv = pythonSet.mkVirtualEnv "bulkgen-env" workspace.deps.default;
|
||||
package = pythonSet.bulkgen;
|
||||
hokusai = mkApplication {
|
||||
venv = pythonSet.mkVirtualEnv "hokusai-env" workspace.deps.default;
|
||||
package = pythonSet.hokusai;
|
||||
};
|
||||
default = bulkgen;
|
||||
default = hokusai;
|
||||
}
|
||||
);
|
||||
|
||||
homeManagerModules = rec {
|
||||
bulkgen = import ./nix/hm-module.nix self;
|
||||
default = bulkgen;
|
||||
hokusai = import ./nix/hm-module.nix self;
|
||||
default = hokusai;
|
||||
};
|
||||
|
||||
checks = forAllSystems (
|
||||
|
|
@ -161,7 +161,7 @@
|
|||
pythonSet = pythonSets.${system};
|
||||
in
|
||||
{
|
||||
inherit (pythonSet.bulkgen.passthru.tests) pytest;
|
||||
inherit (pythonSet.hokusai.passthru.tests) pytest;
|
||||
pre-commit = mkGitHooksCheck system;
|
||||
}
|
||||
);
|
||||
|
|
@ -171,7 +171,7 @@
|
|||
let
|
||||
pkgs = nixpkgs.legacyPackages.${system};
|
||||
pythonSet = pythonSets.${system}.overrideScope editableOverlay;
|
||||
virtualenv = pythonSet.mkVirtualEnv "bulkgen-dev-env" workspace.deps.all;
|
||||
virtualenv = pythonSet.mkVirtualEnv "hokusai-dev-env" workspace.deps.all;
|
||||
in
|
||||
{
|
||||
default = pkgs.mkShell {
|
||||
|
|
|
|||
|
|
@ -9,15 +9,15 @@ from collections.abc import Callable
|
|||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
|
||||
from bulkgen.config import ProjectConfig
|
||||
from bulkgen.graph import build_graph, get_build_order, get_subgraph_for_target
|
||||
from bulkgen.providers import Provider
|
||||
from bulkgen.providers.blackforest import BlackForestProvider
|
||||
from bulkgen.providers.mistral import MistralProvider
|
||||
from bulkgen.providers.openai_image import OpenAIImageProvider
|
||||
from bulkgen.providers.openai_text import OpenAITextProvider
|
||||
from bulkgen.resolve import resolve_model
|
||||
from bulkgen.state import (
|
||||
from hokusai.config import ProjectConfig
|
||||
from hokusai.graph import build_graph, get_build_order, get_subgraph_for_target
|
||||
from hokusai.providers import Provider
|
||||
from hokusai.providers.blackforest import BlackForestProvider
|
||||
from hokusai.providers.mistral import MistralProvider
|
||||
from hokusai.providers.openai_image import OpenAIImageProvider
|
||||
from hokusai.providers.openai_text import OpenAITextProvider
|
||||
from hokusai.resolve import resolve_model
|
||||
from hokusai.state import (
|
||||
BuildState,
|
||||
is_target_dirty,
|
||||
load_state,
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
"""Typer CLI for bulkgen: build, clean, graph commands."""
|
||||
"""Typer CLI for hokusai: build, clean, graph commands."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
|
@ -10,33 +10,33 @@ from typing import Annotated
|
|||
import click
|
||||
import typer
|
||||
|
||||
from bulkgen.builder import BuildEvent, BuildResult, run_build
|
||||
from bulkgen.config import ProjectConfig, load_config
|
||||
from bulkgen.graph import build_graph, get_build_order
|
||||
from bulkgen.providers.registry import get_all_models
|
||||
from bulkgen.state import state_filename
|
||||
from hokusai.builder import BuildEvent, BuildResult, run_build
|
||||
from hokusai.config import ProjectConfig, load_config
|
||||
from hokusai.graph import build_graph, get_build_order
|
||||
from hokusai.providers.registry import get_all_models
|
||||
from hokusai.state import state_filename
|
||||
|
||||
app = typer.Typer(name="bulkgen", help="AI artifact build tool.")
|
||||
app = typer.Typer(name="hokusai", help="AI artifact build tool.")
|
||||
|
||||
_CONFIG_SUFFIX = ".bulkgen.yaml"
|
||||
_CONFIG_SUFFIX = ".hokusai.yaml"
|
||||
|
||||
|
||||
def _project_name(config_path: Path) -> str:
|
||||
"""Derive the project name from a config path.
|
||||
|
||||
``cards.bulkgen.yaml`` → ``cards``
|
||||
``cards.hokusai.yaml`` → ``cards``
|
||||
"""
|
||||
name = config_path.name
|
||||
return name.removesuffix(_CONFIG_SUFFIX)
|
||||
|
||||
|
||||
def _find_config(directory: Path) -> Path:
|
||||
"""Find the single ``*.bulkgen.yaml`` file in *directory*."""
|
||||
candidates = list(directory.glob("*.bulkgen.yaml"))
|
||||
"""Find the single ``*.hokusai.yaml`` file in *directory*."""
|
||||
candidates = list(directory.glob("*.hokusai.yaml"))
|
||||
if len(candidates) == 0:
|
||||
click.echo(
|
||||
click.style("Error: ", fg="red", bold=True)
|
||||
+ "No .bulkgen.yaml file found in current directory",
|
||||
+ "No .hokusai.yaml file found in current directory",
|
||||
err=True,
|
||||
)
|
||||
raise typer.Exit(code=1)
|
||||
|
|
@ -44,7 +44,7 @@ def _find_config(directory: Path) -> Path:
|
|||
names = ", ".join(str(c.name) for c in candidates)
|
||||
click.echo(
|
||||
click.style("Error: ", fg="red", bold=True)
|
||||
+ f"Multiple .bulkgen.yaml files found: {names}",
|
||||
+ f"Multiple .hokusai.yaml files found: {names}",
|
||||
err=True,
|
||||
)
|
||||
raise typer.Exit(code=1)
|
||||
|
|
@ -116,7 +116,7 @@ def build(
|
|||
config = load_config(config_path)
|
||||
name = _project_name(config_path)
|
||||
|
||||
click.echo(click.style("bulkgen", fg="cyan", bold=True) + " building targets...\n")
|
||||
click.echo(click.style("hokusai", fg="cyan", bold=True) + " building targets...\n")
|
||||
|
||||
result, elapsed = _run_build(config, project_dir, name, target)
|
||||
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
"""Pydantic models for bulkgen YAML configuration."""
|
||||
"""Pydantic models for hokusai YAML configuration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
|
@ -9,7 +9,7 @@ from typing import Self
|
|||
import yaml
|
||||
from pydantic import BaseModel, model_validator
|
||||
|
||||
from bulkgen.providers.models import Capability
|
||||
from hokusai.providers.models import Capability
|
||||
|
||||
IMAGE_EXTENSIONS: frozenset[str] = frozenset({".png", ".jpg", ".jpeg", ".webp"})
|
||||
TEXT_EXTENSIONS: frozenset[str] = frozenset({".md", ".txt"})
|
||||
|
|
@ -42,7 +42,7 @@ class TargetConfig(BaseModel):
|
|||
|
||||
|
||||
class ProjectConfig(BaseModel):
|
||||
"""Top-level configuration parsed from ``<name>.bulkgen.yaml``."""
|
||||
"""Top-level configuration parsed from ``<name>.hokusai.yaml``."""
|
||||
|
||||
defaults: Defaults = Defaults()
|
||||
targets: dict[str, TargetConfig]
|
||||
|
|
@ -57,7 +57,7 @@ class ProjectConfig(BaseModel):
|
|||
|
||||
def target_type_from_capabilities(capabilities: frozenset[Capability]) -> TargetType:
|
||||
"""Derive the target type from a set of required capabilities."""
|
||||
from bulkgen.providers.models import Capability
|
||||
from hokusai.providers.models import Capability
|
||||
|
||||
if Capability.TEXT_TO_IMAGE in capabilities:
|
||||
return TargetType.IMAGE
|
||||
|
|
@ -65,7 +65,7 @@ def target_type_from_capabilities(capabilities: frozenset[Capability]) -> Target
|
|||
|
||||
|
||||
def load_config(config_path: Path) -> ProjectConfig:
|
||||
"""Load and validate a ``.bulkgen.yaml`` file."""
|
||||
"""Load and validate a ``.hokusai.yaml`` file."""
|
||||
with config_path.open() as f:
|
||||
raw = yaml.safe_load(f) # pyright: ignore[reportAny]
|
||||
return ProjectConfig.model_validate(raw)
|
||||
|
|
@ -6,7 +6,7 @@ from pathlib import Path
|
|||
|
||||
import networkx as nx
|
||||
|
||||
from bulkgen.config import ProjectConfig
|
||||
from hokusai.config import ProjectConfig
|
||||
|
||||
|
||||
def build_graph(config: ProjectConfig, project_dir: Path) -> nx.DiGraph[str]:
|
||||
|
|
@ -6,10 +6,10 @@ import abc
|
|||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from bulkgen.providers.models import ModelInfo
|
||||
from hokusai.providers.models import ModelInfo
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bulkgen.config import TargetConfig
|
||||
from hokusai.config import TargetConfig
|
||||
|
||||
|
||||
class Provider(abc.ABC):
|
||||
|
|
@ -8,10 +8,10 @@ from typing import override
|
|||
|
||||
import httpx
|
||||
|
||||
from bulkgen.config import TargetConfig
|
||||
from bulkgen.providers import Provider
|
||||
from bulkgen.providers.bfl import BFLClient
|
||||
from bulkgen.providers.models import Capability, ModelInfo
|
||||
from hokusai.config import TargetConfig
|
||||
from hokusai.providers import Provider
|
||||
from hokusai.providers.bfl import BFLClient
|
||||
from hokusai.providers.models import Capability, ModelInfo
|
||||
|
||||
|
||||
def _encode_image_b64(path: Path) -> str:
|
||||
|
|
@ -9,9 +9,9 @@ from typing import override
|
|||
|
||||
from mistralai import Mistral, models
|
||||
|
||||
from bulkgen.config import IMAGE_EXTENSIONS, TargetConfig
|
||||
from bulkgen.providers import Provider
|
||||
from bulkgen.providers.models import Capability, ModelInfo
|
||||
from hokusai.config import IMAGE_EXTENSIONS, TargetConfig
|
||||
from hokusai.providers import Provider
|
||||
from hokusai.providers.models import Capability, ModelInfo
|
||||
|
||||
|
||||
def _image_to_data_url(path: Path) -> str:
|
||||
|
|
@ -128,7 +128,7 @@ def _build_multimodal_message(
|
|||
models.TextChunk(text=prompt),
|
||||
]
|
||||
|
||||
from bulkgen.config import IMAGE_EXTENSIONS
|
||||
from hokusai.config import IMAGE_EXTENSIONS
|
||||
|
||||
for name in input_names:
|
||||
input_path = project_dir / name
|
||||
|
|
@ -10,9 +10,9 @@ import httpx
|
|||
from openai import AsyncOpenAI
|
||||
from openai.types.images_response import ImagesResponse
|
||||
|
||||
from bulkgen.config import TargetConfig
|
||||
from bulkgen.providers import Provider
|
||||
from bulkgen.providers.models import Capability, ModelInfo
|
||||
from hokusai.config import TargetConfig
|
||||
from hokusai.providers import Provider
|
||||
from hokusai.providers.models import Capability, ModelInfo
|
||||
|
||||
_SIZE = Literal[
|
||||
"auto",
|
||||
|
|
@ -15,9 +15,9 @@ from openai.types.chat import (
|
|||
ChatCompletionUserMessageParam,
|
||||
)
|
||||
|
||||
from bulkgen.config import IMAGE_EXTENSIONS, TargetConfig
|
||||
from bulkgen.providers import Provider
|
||||
from bulkgen.providers.models import Capability, ModelInfo
|
||||
from hokusai.config import IMAGE_EXTENSIONS, TargetConfig
|
||||
from hokusai.providers import Provider
|
||||
from hokusai.providers.models import Capability, ModelInfo
|
||||
|
||||
|
||||
def _image_to_data_url(path: Path) -> str:
|
||||
|
|
@ -2,15 +2,15 @@
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
from bulkgen.providers.models import ModelInfo
|
||||
from hokusai.providers.models import ModelInfo
|
||||
|
||||
|
||||
def get_all_models() -> list[ModelInfo]:
|
||||
"""Return the merged list of models from all providers."""
|
||||
from bulkgen.providers.blackforest import BlackForestProvider
|
||||
from bulkgen.providers.mistral import MistralProvider
|
||||
from bulkgen.providers.openai_image import OpenAIImageProvider
|
||||
from bulkgen.providers.openai_text import OpenAITextProvider
|
||||
from hokusai.providers.blackforest import BlackForestProvider
|
||||
from hokusai.providers.mistral import MistralProvider
|
||||
from hokusai.providers.openai_image import OpenAIImageProvider
|
||||
from hokusai.providers.openai_text import OpenAITextProvider
|
||||
|
||||
return (
|
||||
MistralProvider.get_provided_models()
|
||||
|
|
@ -2,7 +2,7 @@ from __future__ import annotations
|
|||
|
||||
from pathlib import Path
|
||||
|
||||
from bulkgen.config import (
|
||||
from hokusai.config import (
|
||||
IMAGE_EXTENSIONS,
|
||||
TEXT_EXTENSIONS,
|
||||
Defaults,
|
||||
|
|
@ -10,7 +10,7 @@ from bulkgen.config import (
|
|||
TargetType,
|
||||
target_type_from_capabilities,
|
||||
)
|
||||
from bulkgen.providers.models import Capability, ModelInfo
|
||||
from hokusai.providers.models import Capability, ModelInfo
|
||||
|
||||
|
||||
def infer_required_capabilities(
|
||||
|
|
@ -53,7 +53,7 @@ def resolve_model(
|
|||
|
||||
Raises :class:`ValueError` if no suitable model can be found.
|
||||
"""
|
||||
from bulkgen.providers.registry import get_all_models
|
||||
from hokusai.providers.registry import get_all_models
|
||||
|
||||
all_models = get_all_models()
|
||||
required = infer_required_capabilities(target_name, target)
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
"""Incremental build state tracking via ``.<project>.bulkgen-state.yaml``."""
|
||||
"""Incremental build state tracking via ``.<project>.hokusai-state.yaml``."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
|
@ -12,10 +12,10 @@ from pydantic import BaseModel
|
|||
def state_filename(project_name: str) -> str:
|
||||
"""Return the state filename for a given project name.
|
||||
|
||||
For a config file named ``cards.bulkgen.yaml`` the project name is
|
||||
``cards`` and the state file is ``.cards.bulkgen-state.yaml``.
|
||||
For a config file named ``cards.hokusai.yaml`` the project name is
|
||||
``cards`` and the state file is ``.cards.hokusai-state.yaml``.
|
||||
"""
|
||||
return f".{project_name}.bulkgen-state.yaml"
|
||||
return f".{project_name}.hokusai-state.yaml"
|
||||
|
||||
|
||||
class TargetState(BaseModel):
|
||||
4
main.py
4
main.py
|
|
@ -1,6 +1,6 @@
|
|||
"""Entry point for the bulkgen CLI."""
|
||||
"""Entry point for the hokusai CLI."""
|
||||
|
||||
from bulkgen.cli import app
|
||||
from hokusai.cli import app
|
||||
|
||||
|
||||
def main() -> None:
|
||||
|
|
|
|||
|
|
@ -6,12 +6,12 @@ self:
|
|||
...
|
||||
}:
|
||||
let
|
||||
cfg = config.programs.bulkgen;
|
||||
cfg = config.programs.hokusai;
|
||||
in
|
||||
{
|
||||
options.programs.bulkgen = {
|
||||
enable = lib.mkEnableOption "bulkgen";
|
||||
package = lib.mkPackageOption self.packages.${pkgs.system} "bulkgen" { };
|
||||
options.programs.hokusai = {
|
||||
enable = lib.mkEnableOption "hokusai";
|
||||
package = lib.mkPackageOption self.packages.${pkgs.system} "hokusai" { };
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
[project]
|
||||
name = "bulkgen"
|
||||
name = "hokusai"
|
||||
version = "0.1.0"
|
||||
description = "Bulk-Generate Images with Generative AI"
|
||||
readme = "README.md"
|
||||
|
|
@ -16,7 +16,7 @@ dependencies = [
|
|||
]
|
||||
|
||||
[project.scripts]
|
||||
bulkgen = "bulkgen.cli:app"
|
||||
hokusai = "hokusai.cli:app"
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
"""Shared fixtures for bulkgen integration tests."""
|
||||
"""Shared fixtures for hokusai integration tests."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
|
@ -8,7 +8,7 @@ from pathlib import Path
|
|||
import pytest
|
||||
import yaml
|
||||
|
||||
from bulkgen.config import ProjectConfig, load_config
|
||||
from hokusai.config import ProjectConfig, load_config
|
||||
|
||||
WriteConfig = Callable[[dict[str, object]], ProjectConfig]
|
||||
|
||||
|
|
@ -21,7 +21,7 @@ def project_dir(tmp_path: Path) -> Path:
|
|||
|
||||
@pytest.fixture
|
||||
def write_config(project_dir: Path) -> WriteConfig:
|
||||
"""Write a bulkgen YAML config and return the loaded ProjectConfig.
|
||||
"""Write a hokusai YAML config and return the loaded ProjectConfig.
|
||||
|
||||
Usage::
|
||||
|
||||
|
|
@ -29,7 +29,7 @@ def write_config(project_dir: Path) -> WriteConfig:
|
|||
"""
|
||||
|
||||
def _write(raw: dict[str, object]) -> ProjectConfig:
|
||||
config_path = project_dir / "project.bulkgen.yaml"
|
||||
config_path = project_dir / "project.hokusai.yaml"
|
||||
_ = config_path.write_text(yaml.dump(raw, default_flow_style=False))
|
||||
return load_config(config_path)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
"""Integration tests for bulkgen.builder."""
|
||||
"""Integration tests for hokusai.builder."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
|
@ -9,17 +9,17 @@ from unittest.mock import patch
|
|||
|
||||
import pytest
|
||||
|
||||
from bulkgen.builder import (
|
||||
from hokusai.builder import (
|
||||
_collect_all_deps, # pyright: ignore[reportPrivateUsage]
|
||||
_collect_dep_files, # pyright: ignore[reportPrivateUsage]
|
||||
_collect_extra_params, # pyright: ignore[reportPrivateUsage]
|
||||
_resolve_prompt, # pyright: ignore[reportPrivateUsage]
|
||||
run_build,
|
||||
)
|
||||
from bulkgen.config import ProjectConfig, TargetConfig
|
||||
from bulkgen.providers import Provider
|
||||
from bulkgen.providers.models import Capability, ModelInfo
|
||||
from bulkgen.state import load_state
|
||||
from hokusai.config import ProjectConfig, TargetConfig
|
||||
from hokusai.providers import Provider
|
||||
from hokusai.providers.models import Capability, ModelInfo
|
||||
from hokusai.state import load_state
|
||||
|
||||
WriteConfig = Callable[[dict[str, object]], ProjectConfig]
|
||||
|
||||
|
|
@ -197,7 +197,7 @@ class TestRunBuild:
|
|||
async def test_build_single_text_target(
|
||||
self, project_dir: Path, simple_text_config: ProjectConfig
|
||||
) -> None:
|
||||
with patch("bulkgen.builder._create_providers", return_value=_fake_providers()):
|
||||
with patch("hokusai.builder._create_providers", return_value=_fake_providers()):
|
||||
result = await run_build(simple_text_config, project_dir, _PROJECT)
|
||||
|
||||
assert result.built == ["output.txt"]
|
||||
|
|
@ -208,7 +208,7 @@ class TestRunBuild:
|
|||
async def test_build_chain_dependency(
|
||||
self, project_dir: Path, multi_target_config: ProjectConfig
|
||||
) -> None:
|
||||
with patch("bulkgen.builder._create_providers", return_value=_fake_providers()):
|
||||
with patch("hokusai.builder._create_providers", return_value=_fake_providers()):
|
||||
result = await run_build(multi_target_config, project_dir, _PROJECT)
|
||||
|
||||
assert "summary.md" in result.built
|
||||
|
|
@ -223,7 +223,7 @@ class TestRunBuild:
|
|||
async def test_incremental_build_skips_clean_targets(
|
||||
self, project_dir: Path, simple_text_config: ProjectConfig
|
||||
) -> None:
|
||||
with patch("bulkgen.builder._create_providers", return_value=_fake_providers()):
|
||||
with patch("hokusai.builder._create_providers", return_value=_fake_providers()):
|
||||
result1 = await run_build(simple_text_config, project_dir, _PROJECT)
|
||||
assert result1.built == ["output.txt"]
|
||||
|
||||
|
|
@ -235,7 +235,7 @@ class TestRunBuild:
|
|||
self, project_dir: Path, write_config: WriteConfig
|
||||
) -> None:
|
||||
config1 = write_config({"targets": {"out.txt": {"prompt": "version 1"}}})
|
||||
with patch("bulkgen.builder._create_providers", return_value=_fake_providers()):
|
||||
with patch("hokusai.builder._create_providers", return_value=_fake_providers()):
|
||||
r1 = await run_build(config1, project_dir, _PROJECT)
|
||||
assert r1.built == ["out.txt"]
|
||||
|
||||
|
|
@ -250,7 +250,7 @@ class TestRunBuild:
|
|||
config = write_config(
|
||||
{"targets": {"out.md": {"prompt": "x", "inputs": ["data.txt"]}}}
|
||||
)
|
||||
with patch("bulkgen.builder._create_providers", return_value=_fake_providers()):
|
||||
with patch("hokusai.builder._create_providers", return_value=_fake_providers()):
|
||||
r1 = await run_build(config, project_dir, _PROJECT)
|
||||
assert r1.built == ["out.md"]
|
||||
|
||||
|
|
@ -261,7 +261,7 @@ class TestRunBuild:
|
|||
async def test_selective_build_single_target(
|
||||
self, project_dir: Path, multi_target_config: ProjectConfig
|
||||
) -> None:
|
||||
with patch("bulkgen.builder._create_providers", return_value=_fake_providers()):
|
||||
with patch("hokusai.builder._create_providers", return_value=_fake_providers()):
|
||||
result = await run_build(
|
||||
multi_target_config, project_dir, _PROJECT, target="summary.md"
|
||||
)
|
||||
|
|
@ -273,7 +273,7 @@ class TestRunBuild:
|
|||
async def test_selective_build_unknown_target_raises(
|
||||
self, project_dir: Path, simple_text_config: ProjectConfig
|
||||
) -> None:
|
||||
with patch("bulkgen.builder._create_providers", return_value=_fake_providers()):
|
||||
with patch("hokusai.builder._create_providers", return_value=_fake_providers()):
|
||||
with pytest.raises(ValueError, match="Unknown target"):
|
||||
_ = await run_build(
|
||||
simple_text_config, project_dir, _PROJECT, target="nonexistent.txt"
|
||||
|
|
@ -321,7 +321,7 @@ class TestRunBuild:
|
|||
routing_provider.generate = selective_generate # type: ignore[assignment]
|
||||
|
||||
with patch(
|
||||
"bulkgen.builder._create_providers",
|
||||
"hokusai.builder._create_providers",
|
||||
return_value=[routing_provider, FakeImageProvider()],
|
||||
):
|
||||
result = await run_build(config, project_dir, _PROJECT)
|
||||
|
|
@ -342,7 +342,7 @@ class TestRunBuild:
|
|||
)
|
||||
|
||||
with patch(
|
||||
"bulkgen.builder._create_providers",
|
||||
"hokusai.builder._create_providers",
|
||||
return_value=[FailingTextProvider(), FakeImageProvider()],
|
||||
):
|
||||
result = await run_build(config, project_dir, _PROJECT)
|
||||
|
|
@ -355,7 +355,7 @@ class TestRunBuild:
|
|||
self, project_dir: Path, simple_text_config: ProjectConfig
|
||||
) -> None:
|
||||
with patch(
|
||||
"bulkgen.builder._create_providers",
|
||||
"hokusai.builder._create_providers",
|
||||
return_value=[],
|
||||
):
|
||||
result = await run_build(simple_text_config, project_dir, _PROJECT)
|
||||
|
|
@ -374,7 +374,7 @@ class TestRunBuild:
|
|||
}
|
||||
}
|
||||
)
|
||||
with patch("bulkgen.builder._create_providers", return_value=_fake_providers()):
|
||||
with patch("hokusai.builder._create_providers", return_value=_fake_providers()):
|
||||
_ = await run_build(config, project_dir, _PROJECT)
|
||||
|
||||
state = load_state(project_dir, _PROJECT)
|
||||
|
|
@ -386,7 +386,7 @@ class TestRunBuild:
|
|||
) -> None:
|
||||
config = write_config({"targets": {"out.txt": {"prompt": prompt_file.name}}})
|
||||
|
||||
with patch("bulkgen.builder._create_providers", return_value=_fake_providers()):
|
||||
with patch("hokusai.builder._create_providers", return_value=_fake_providers()):
|
||||
result = await run_build(config, project_dir, _PROJECT)
|
||||
|
||||
assert result.built == ["out.txt"]
|
||||
|
|
@ -396,7 +396,7 @@ class TestRunBuild:
|
|||
async def test_rebuild_after_output_deleted(
|
||||
self, project_dir: Path, simple_text_config: ProjectConfig
|
||||
) -> None:
|
||||
with patch("bulkgen.builder._create_providers", return_value=_fake_providers()):
|
||||
with patch("hokusai.builder._create_providers", return_value=_fake_providers()):
|
||||
r1 = await run_build(simple_text_config, project_dir, _PROJECT)
|
||||
assert r1.built == ["output.txt"]
|
||||
|
||||
|
|
@ -421,7 +421,7 @@ class TestRunBuild:
|
|||
}
|
||||
}
|
||||
)
|
||||
with patch("bulkgen.builder._create_providers", return_value=_fake_providers()):
|
||||
with patch("hokusai.builder._create_providers", return_value=_fake_providers()):
|
||||
result = await run_build(config, project_dir, _PROJECT)
|
||||
|
||||
assert set(result.built) == {"left.md", "right.md", "merge.txt"}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
"""Integration tests for bulkgen.cli.
|
||||
"""Integration tests for hokusai.cli.
|
||||
|
||||
Patching ``Path.cwd()`` produces Any-typed return values from mock objects.
|
||||
"""
|
||||
|
|
@ -13,8 +13,8 @@ import pytest
|
|||
import yaml
|
||||
from typer.testing import CliRunner
|
||||
|
||||
from bulkgen.builder import BuildResult
|
||||
from bulkgen.cli import app
|
||||
from hokusai.builder import BuildResult
|
||||
from hokusai.cli import app
|
||||
|
||||
runner = CliRunner()
|
||||
|
||||
|
|
@ -28,7 +28,7 @@ def cli_project(tmp_path: Path) -> Path:
|
|||
"image.png": {"prompt": "Generate image"},
|
||||
}
|
||||
}
|
||||
_ = (tmp_path / "project.bulkgen.yaml").write_text(
|
||||
_ = (tmp_path / "project.hokusai.yaml").write_text(
|
||||
yaml.dump(config, default_flow_style=False)
|
||||
)
|
||||
return tmp_path
|
||||
|
|
@ -38,24 +38,24 @@ class TestFindConfig:
|
|||
"""Test config file discovery."""
|
||||
|
||||
def test_no_config_file(self, tmp_path: Path) -> None:
|
||||
with patch("bulkgen.cli.Path") as mock_path_cls:
|
||||
with patch("hokusai.cli.Path") as mock_path_cls:
|
||||
mock_path_cls.cwd.return_value = tmp_path
|
||||
result = runner.invoke(app, ["build"])
|
||||
assert result.exit_code != 0
|
||||
assert "No .bulkgen.yaml file found" in result.output
|
||||
assert "No .hokusai.yaml file found" in result.output
|
||||
|
||||
def test_multiple_config_files(self, tmp_path: Path) -> None:
|
||||
_ = (tmp_path / "a.bulkgen.yaml").write_text(
|
||||
_ = (tmp_path / "a.hokusai.yaml").write_text(
|
||||
yaml.dump({"targets": {"x.txt": {"prompt": "a"}}})
|
||||
)
|
||||
_ = (tmp_path / "b.bulkgen.yaml").write_text(
|
||||
_ = (tmp_path / "b.hokusai.yaml").write_text(
|
||||
yaml.dump({"targets": {"y.txt": {"prompt": "b"}}})
|
||||
)
|
||||
with patch("bulkgen.cli.Path") as mock_path_cls:
|
||||
with patch("hokusai.cli.Path") as mock_path_cls:
|
||||
mock_path_cls.cwd.return_value = tmp_path
|
||||
result = runner.invoke(app, ["build"])
|
||||
assert result.exit_code != 0
|
||||
assert "Multiple .bulkgen.yaml files found" in result.output
|
||||
assert "Multiple .hokusai.yaml files found" in result.output
|
||||
|
||||
|
||||
class TestBuildCommand:
|
||||
|
|
@ -66,9 +66,9 @@ class TestBuildCommand:
|
|||
built=["output.txt", "image.png"], skipped=[], failed={}
|
||||
)
|
||||
with (
|
||||
patch("bulkgen.cli.Path") as mock_path_cls,
|
||||
patch("hokusai.cli.Path") as mock_path_cls,
|
||||
patch(
|
||||
"bulkgen.cli.run_build",
|
||||
"hokusai.cli.run_build",
|
||||
new_callable=AsyncMock,
|
||||
return_value=build_result,
|
||||
),
|
||||
|
|
@ -84,9 +84,9 @@ class TestBuildCommand:
|
|||
built=[], skipped=["output.txt", "image.png"], failed={}
|
||||
)
|
||||
with (
|
||||
patch("bulkgen.cli.Path") as mock_path_cls,
|
||||
patch("hokusai.cli.Path") as mock_path_cls,
|
||||
patch(
|
||||
"bulkgen.cli.run_build",
|
||||
"hokusai.cli.run_build",
|
||||
new_callable=AsyncMock,
|
||||
return_value=build_result,
|
||||
),
|
||||
|
|
@ -104,9 +104,9 @@ class TestBuildCommand:
|
|||
failed={"image.png": "Missing BFL_API_KEY"},
|
||||
)
|
||||
with (
|
||||
patch("bulkgen.cli.Path") as mock_path_cls,
|
||||
patch("hokusai.cli.Path") as mock_path_cls,
|
||||
patch(
|
||||
"bulkgen.cli.run_build",
|
||||
"hokusai.cli.run_build",
|
||||
new_callable=AsyncMock,
|
||||
return_value=build_result,
|
||||
),
|
||||
|
|
@ -120,9 +120,9 @@ class TestBuildCommand:
|
|||
def test_build_specific_target(self, cli_project: Path) -> None:
|
||||
build_result = BuildResult(built=["output.txt"], skipped=[], failed={})
|
||||
with (
|
||||
patch("bulkgen.cli.Path") as mock_path_cls,
|
||||
patch("hokusai.cli.Path") as mock_path_cls,
|
||||
patch(
|
||||
"bulkgen.cli.run_build",
|
||||
"hokusai.cli.run_build",
|
||||
new_callable=AsyncMock,
|
||||
return_value=build_result,
|
||||
) as mock_run,
|
||||
|
|
@ -142,10 +142,10 @@ class TestCleanCommand:
|
|||
def test_clean_removes_targets(self, cli_project: Path) -> None:
|
||||
_ = (cli_project / "output.txt").write_text("generated")
|
||||
_ = (cli_project / "image.png").write_bytes(b"\x89PNG")
|
||||
state_file = ".project.bulkgen-state.yaml"
|
||||
state_file = ".project.hokusai-state.yaml"
|
||||
_ = (cli_project / state_file).write_text("targets: {}")
|
||||
|
||||
with patch("bulkgen.cli.Path") as mock_path_cls:
|
||||
with patch("hokusai.cli.Path") as mock_path_cls:
|
||||
mock_path_cls.cwd.return_value = cli_project
|
||||
result = runner.invoke(app, ["clean"])
|
||||
|
||||
|
|
@ -156,7 +156,7 @@ class TestCleanCommand:
|
|||
assert "Cleaned 2 artifact(s)" in result.output
|
||||
|
||||
def test_clean_no_artifacts(self, cli_project: Path) -> None:
|
||||
with patch("bulkgen.cli.Path") as mock_path_cls:
|
||||
with patch("hokusai.cli.Path") as mock_path_cls:
|
||||
mock_path_cls.cwd.return_value = cli_project
|
||||
result = runner.invoke(app, ["clean"])
|
||||
|
||||
|
|
@ -166,7 +166,7 @@ class TestCleanCommand:
|
|||
def test_clean_partial_artifacts(self, cli_project: Path) -> None:
|
||||
_ = (cli_project / "output.txt").write_text("generated")
|
||||
|
||||
with patch("bulkgen.cli.Path") as mock_path_cls:
|
||||
with patch("hokusai.cli.Path") as mock_path_cls:
|
||||
mock_path_cls.cwd.return_value = cli_project
|
||||
result = runner.invoke(app, ["clean"])
|
||||
|
||||
|
|
@ -180,10 +180,10 @@ class TestGraphCommand:
|
|||
|
||||
def test_graph_single_target(self, tmp_path: Path) -> None:
|
||||
config = {"targets": {"out.txt": {"prompt": "hello"}}}
|
||||
_ = (tmp_path / "test.bulkgen.yaml").write_text(
|
||||
_ = (tmp_path / "test.hokusai.yaml").write_text(
|
||||
yaml.dump(config, default_flow_style=False)
|
||||
)
|
||||
with patch("bulkgen.cli.Path") as mock_path_cls:
|
||||
with patch("hokusai.cli.Path") as mock_path_cls:
|
||||
mock_path_cls.cwd.return_value = tmp_path
|
||||
result = runner.invoke(app, ["graph"])
|
||||
|
||||
|
|
@ -198,10 +198,10 @@ class TestGraphCommand:
|
|||
"step2.txt": {"prompt": "y", "inputs": ["step1.md"]},
|
||||
}
|
||||
}
|
||||
_ = (tmp_path / "test.bulkgen.yaml").write_text(
|
||||
_ = (tmp_path / "test.hokusai.yaml").write_text(
|
||||
yaml.dump(config, default_flow_style=False)
|
||||
)
|
||||
with patch("bulkgen.cli.Path") as mock_path_cls:
|
||||
with patch("hokusai.cli.Path") as mock_path_cls:
|
||||
mock_path_cls.cwd.return_value = tmp_path
|
||||
result = runner.invoke(app, ["graph"])
|
||||
|
||||
|
|
@ -219,10 +219,10 @@ class TestGraphCommand:
|
|||
"b.txt": {"prompt": "y", "inputs": ["a.txt"]},
|
||||
}
|
||||
}
|
||||
_ = (tmp_path / "test.bulkgen.yaml").write_text(
|
||||
_ = (tmp_path / "test.hokusai.yaml").write_text(
|
||||
yaml.dump(config, default_flow_style=False)
|
||||
)
|
||||
with patch("bulkgen.cli.Path") as mock_path_cls:
|
||||
with patch("hokusai.cli.Path") as mock_path_cls:
|
||||
mock_path_cls.cwd.return_value = tmp_path
|
||||
result = runner.invoke(app, ["graph"])
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
"""Integration tests for bulkgen.config."""
|
||||
"""Integration tests for hokusai.config."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
|
@ -7,14 +7,14 @@ from pathlib import Path
|
|||
import pytest
|
||||
import yaml
|
||||
|
||||
from bulkgen.config import load_config
|
||||
from hokusai.config import load_config
|
||||
|
||||
|
||||
class TestLoadConfig:
|
||||
"""Test loading and validating YAML config files end-to-end."""
|
||||
|
||||
def test_minimal_config(self, project_dir: Path) -> None:
|
||||
config_path = project_dir / "test.bulkgen.yaml"
|
||||
config_path = project_dir / "test.hokusai.yaml"
|
||||
_ = config_path.write_text(
|
||||
yaml.dump({"targets": {"out.txt": {"prompt": "hello"}}})
|
||||
)
|
||||
|
|
@ -47,7 +47,7 @@ class TestLoadConfig:
|
|||
},
|
||||
},
|
||||
}
|
||||
config_path = project_dir / "full.bulkgen.yaml"
|
||||
config_path = project_dir / "full.hokusai.yaml"
|
||||
_ = config_path.write_text(yaml.dump(raw, default_flow_style=False))
|
||||
|
||||
config = load_config(config_path)
|
||||
|
|
@ -67,14 +67,14 @@ class TestLoadConfig:
|
|||
assert story.inputs == ["banner.png"]
|
||||
|
||||
def test_empty_targets_rejected(self, project_dir: Path) -> None:
|
||||
config_path = project_dir / "empty.bulkgen.yaml"
|
||||
config_path = project_dir / "empty.hokusai.yaml"
|
||||
_ = config_path.write_text(yaml.dump({"targets": {}}))
|
||||
|
||||
with pytest.raises(Exception, match="At least one target"):
|
||||
_ = load_config(config_path)
|
||||
|
||||
def test_missing_prompt_rejected(self, project_dir: Path) -> None:
|
||||
config_path = project_dir / "bad.bulkgen.yaml"
|
||||
config_path = project_dir / "bad.hokusai.yaml"
|
||||
_ = config_path.write_text(yaml.dump({"targets": {"out.txt": {}}}))
|
||||
|
||||
with pytest.raises(Exception):
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
"""Integration tests for bulkgen.graph."""
|
||||
"""Integration tests for hokusai.graph."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
|
@ -7,8 +7,8 @@ from pathlib import Path
|
|||
|
||||
import pytest
|
||||
|
||||
from bulkgen.config import ProjectConfig
|
||||
from bulkgen.graph import build_graph, get_build_order, get_subgraph_for_target
|
||||
from hokusai.config import ProjectConfig
|
||||
from hokusai.graph import build_graph, get_build_order, get_subgraph_for_target
|
||||
|
||||
WriteConfig = Callable[[dict[str, object]], ProjectConfig]
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
"""Integration tests for bulkgen.providers (image and text).
|
||||
"""Integration tests for hokusai.providers (image and text).
|
||||
|
||||
Mock-heavy tests produce many Any-typed expressions from MagicMock.
|
||||
"""
|
||||
|
|
@ -12,16 +12,16 @@ from unittest.mock import AsyncMock, MagicMock, patch
|
|||
|
||||
import pytest
|
||||
|
||||
from bulkgen.config import TargetConfig
|
||||
from bulkgen.providers.bfl import BFLResult
|
||||
from bulkgen.providers.blackforest import BlackForestProvider
|
||||
from bulkgen.providers.blackforest import (
|
||||
from hokusai.config import TargetConfig
|
||||
from hokusai.providers.bfl import BFLResult
|
||||
from hokusai.providers.blackforest import BlackForestProvider
|
||||
from hokusai.providers.blackforest import (
|
||||
_encode_image_b64 as encode_image_b64, # pyright: ignore[reportPrivateUsage]
|
||||
)
|
||||
from bulkgen.providers.mistral import MistralProvider
|
||||
from bulkgen.providers.models import ModelInfo
|
||||
from bulkgen.providers.openai_image import OpenAIImageProvider
|
||||
from bulkgen.providers.registry import get_all_models
|
||||
from hokusai.providers.mistral import MistralProvider
|
||||
from hokusai.providers.models import ModelInfo
|
||||
from hokusai.providers.openai_image import OpenAIImageProvider
|
||||
from hokusai.providers.registry import get_all_models
|
||||
|
||||
|
||||
def _model(name: str) -> ModelInfo:
|
||||
|
|
@ -85,8 +85,8 @@ class TestBlackForestProvider:
|
|||
bfl_result, mock_http = _make_bfl_mocks(image_bytes)
|
||||
|
||||
with (
|
||||
patch("bulkgen.providers.blackforest.BFLClient") as mock_cls,
|
||||
patch("bulkgen.providers.blackforest.httpx.AsyncClient") as mock_http_cls,
|
||||
patch("hokusai.providers.blackforest.BFLClient") as mock_cls,
|
||||
patch("hokusai.providers.blackforest.httpx.AsyncClient") as mock_http_cls,
|
||||
):
|
||||
mock_cls.return_value.generate = AsyncMock(return_value=bfl_result)
|
||||
mock_http_cls.return_value = mock_http
|
||||
|
|
@ -111,8 +111,8 @@ class TestBlackForestProvider:
|
|||
bfl_result, mock_http = _make_bfl_mocks(image_bytes)
|
||||
|
||||
with (
|
||||
patch("bulkgen.providers.blackforest.BFLClient") as mock_cls,
|
||||
patch("bulkgen.providers.blackforest.httpx.AsyncClient") as mock_http_cls,
|
||||
patch("hokusai.providers.blackforest.BFLClient") as mock_cls,
|
||||
patch("hokusai.providers.blackforest.httpx.AsyncClient") as mock_http_cls,
|
||||
):
|
||||
mock_generate = AsyncMock(return_value=bfl_result)
|
||||
mock_cls.return_value.generate = mock_generate
|
||||
|
|
@ -142,8 +142,8 @@ class TestBlackForestProvider:
|
|||
bfl_result, mock_http = _make_bfl_mocks(image_bytes)
|
||||
|
||||
with (
|
||||
patch("bulkgen.providers.blackforest.BFLClient") as mock_cls,
|
||||
patch("bulkgen.providers.blackforest.httpx.AsyncClient") as mock_http_cls,
|
||||
patch("hokusai.providers.blackforest.BFLClient") as mock_cls,
|
||||
patch("hokusai.providers.blackforest.httpx.AsyncClient") as mock_http_cls,
|
||||
):
|
||||
mock_generate = AsyncMock(return_value=bfl_result)
|
||||
mock_cls.return_value.generate = mock_generate
|
||||
|
|
@ -177,8 +177,8 @@ class TestBlackForestProvider:
|
|||
bfl_result, mock_http = _make_bfl_mocks(image_bytes)
|
||||
|
||||
with (
|
||||
patch("bulkgen.providers.blackforest.BFLClient") as mock_cls,
|
||||
patch("bulkgen.providers.blackforest.httpx.AsyncClient") as mock_http_cls,
|
||||
patch("hokusai.providers.blackforest.BFLClient") as mock_cls,
|
||||
patch("hokusai.providers.blackforest.httpx.AsyncClient") as mock_http_cls,
|
||||
):
|
||||
mock_generate = AsyncMock(return_value=bfl_result)
|
||||
mock_cls.return_value.generate = mock_generate
|
||||
|
|
@ -208,8 +208,8 @@ class TestBlackForestProvider:
|
|||
bfl_result, mock_http = _make_bfl_mocks(image_bytes)
|
||||
|
||||
with (
|
||||
patch("bulkgen.providers.blackforest.BFLClient") as mock_cls,
|
||||
patch("bulkgen.providers.blackforest.httpx.AsyncClient") as mock_http_cls,
|
||||
patch("hokusai.providers.blackforest.BFLClient") as mock_cls,
|
||||
patch("hokusai.providers.blackforest.httpx.AsyncClient") as mock_http_cls,
|
||||
):
|
||||
mock_generate = AsyncMock(return_value=bfl_result)
|
||||
mock_cls.return_value.generate = mock_generate
|
||||
|
|
@ -231,8 +231,8 @@ class TestBlackForestProvider:
|
|||
async def test_image_no_sample_url_raises(self, project_dir: Path) -> None:
|
||||
target_config = TargetConfig(prompt="x")
|
||||
|
||||
with patch("bulkgen.providers.blackforest.BFLClient") as mock_cls:
|
||||
from bulkgen.providers.bfl import BFLError
|
||||
with patch("hokusai.providers.blackforest.BFLClient") as mock_cls:
|
||||
from hokusai.providers.bfl import BFLError
|
||||
|
||||
mock_cls.return_value.generate = AsyncMock(
|
||||
side_effect=BFLError("BFL task test ready but no sample URL: {}")
|
||||
|
|
@ -264,7 +264,7 @@ class TestMistralProvider:
|
|||
target_config = TargetConfig(prompt="Write a poem")
|
||||
response = _make_text_response("Roses are red...")
|
||||
|
||||
with patch("bulkgen.providers.mistral.Mistral") as mock_cls:
|
||||
with patch("hokusai.providers.mistral.Mistral") as mock_cls:
|
||||
mock_cls.return_value = _make_mistral_mock(response)
|
||||
|
||||
provider = MistralProvider(api_key="test-key")
|
||||
|
|
@ -285,7 +285,7 @@ class TestMistralProvider:
|
|||
target_config = TargetConfig(prompt="Summarize", inputs=["source.txt"])
|
||||
response = _make_text_response("Summary: ...")
|
||||
|
||||
with patch("bulkgen.providers.mistral.Mistral") as mock_cls:
|
||||
with patch("hokusai.providers.mistral.Mistral") as mock_cls:
|
||||
mock_client = _make_mistral_mock(response)
|
||||
mock_cls.return_value = mock_client
|
||||
|
||||
|
|
@ -309,7 +309,7 @@ class TestMistralProvider:
|
|||
target_config = TargetConfig(prompt="Describe this image", inputs=["photo.png"])
|
||||
response = _make_text_response("A beautiful photo")
|
||||
|
||||
with patch("bulkgen.providers.mistral.Mistral") as mock_cls:
|
||||
with patch("hokusai.providers.mistral.Mistral") as mock_cls:
|
||||
mock_client = _make_mistral_mock(response)
|
||||
mock_cls.return_value = mock_client
|
||||
|
||||
|
|
@ -334,7 +334,7 @@ class TestMistralProvider:
|
|||
response = MagicMock()
|
||||
response.choices = []
|
||||
|
||||
with patch("bulkgen.providers.mistral.Mistral") as mock_cls:
|
||||
with patch("hokusai.providers.mistral.Mistral") as mock_cls:
|
||||
mock_cls.return_value = _make_mistral_mock(response)
|
||||
|
||||
provider = MistralProvider(api_key="test-key")
|
||||
|
|
@ -351,7 +351,7 @@ class TestMistralProvider:
|
|||
target_config = TargetConfig(prompt="x")
|
||||
response = _make_text_response(None)
|
||||
|
||||
with patch("bulkgen.providers.mistral.Mistral") as mock_cls:
|
||||
with patch("hokusai.providers.mistral.Mistral") as mock_cls:
|
||||
mock_cls.return_value = _make_mistral_mock(response)
|
||||
|
||||
provider = MistralProvider(api_key="test-key")
|
||||
|
|
@ -374,7 +374,7 @@ class TestMistralProvider:
|
|||
)
|
||||
response = _make_text_response("Combined")
|
||||
|
||||
with patch("bulkgen.providers.mistral.Mistral") as mock_cls:
|
||||
with patch("hokusai.providers.mistral.Mistral") as mock_cls:
|
||||
mock_client = _make_mistral_mock(response)
|
||||
mock_cls.return_value = mock_client
|
||||
|
||||
|
|
@ -405,7 +405,7 @@ class TestMistralProvider:
|
|||
)
|
||||
response = _make_text_response("A stylized image")
|
||||
|
||||
with patch("bulkgen.providers.mistral.Mistral") as mock_cls:
|
||||
with patch("hokusai.providers.mistral.Mistral") as mock_cls:
|
||||
mock_client = _make_mistral_mock(response)
|
||||
mock_cls.return_value = mock_client
|
||||
|
||||
|
|
@ -459,7 +459,7 @@ class TestOpenAIImageProvider:
|
|||
b64 = base64.b64encode(image_bytes).decode()
|
||||
mock_client = _make_openai_mock(b64)
|
||||
|
||||
with patch("bulkgen.providers.openai_image.AsyncOpenAI") as mock_cls:
|
||||
with patch("hokusai.providers.openai_image.AsyncOpenAI") as mock_cls:
|
||||
mock_cls.return_value = mock_client
|
||||
|
||||
provider = OpenAIImageProvider(api_key="test-key")
|
||||
|
|
@ -493,7 +493,7 @@ class TestOpenAIImageProvider:
|
|||
b64 = base64.b64encode(image_bytes).decode()
|
||||
mock_client = _make_openai_mock(b64)
|
||||
|
||||
with patch("bulkgen.providers.openai_image.AsyncOpenAI") as mock_cls:
|
||||
with patch("hokusai.providers.openai_image.AsyncOpenAI") as mock_cls:
|
||||
mock_cls.return_value = mock_client
|
||||
|
||||
provider = OpenAIImageProvider(api_key="test-key")
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
"""Integration tests for bulkgen.config."""
|
||||
"""Integration tests for hokusai.config."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from bulkgen.config import (
|
||||
from hokusai.config import (
|
||||
Defaults,
|
||||
TargetConfig,
|
||||
)
|
||||
from bulkgen.providers.models import Capability
|
||||
from bulkgen.resolve import infer_required_capabilities, resolve_model
|
||||
from hokusai.providers.models import Capability
|
||||
from hokusai.resolve import infer_required_capabilities, resolve_model
|
||||
|
||||
|
||||
class TestInferRequiredCapabilities:
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
"""Integration tests for bulkgen.state."""
|
||||
"""Integration tests for hokusai.state."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
|
@ -6,7 +6,7 @@ from pathlib import Path
|
|||
|
||||
import yaml
|
||||
|
||||
from bulkgen.state import (
|
||||
from hokusai.state import (
|
||||
BuildState,
|
||||
TargetState,
|
||||
hash_file,
|
||||
|
|
@ -41,10 +41,10 @@ class TestStateFilename:
|
|||
"""Test state filename derivation."""
|
||||
|
||||
def test_state_filename(self) -> None:
|
||||
assert state_filename("cards") == ".cards.bulkgen-state.yaml"
|
||||
assert state_filename("cards") == ".cards.hokusai-state.yaml"
|
||||
|
||||
def test_state_filename_simple(self) -> None:
|
||||
assert state_filename("project") == ".project.bulkgen-state.yaml"
|
||||
assert state_filename("project") == ".project.hokusai-state.yaml"
|
||||
|
||||
|
||||
class TestStatePersistence:
|
||||
|
|
|
|||
2
uv.lock
generated
2
uv.lock
generated
|
|
@ -45,7 +45,7 @@ wheels = [
|
|||
]
|
||||
|
||||
[[package]]
|
||||
name = "bulkgen"
|
||||
name = "hokusai"
|
||||
version = "0.1.0"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue