feat: add pydantic config models for YAML parsing

- TargetType enum with IMAGE/TEXT variants
- Defaults, TargetConfig, ProjectConfig pydantic models
- infer_target_type() for extension-based type dispatch
- resolve_model() for default/override model resolution
- load_config() using yaml.safe_load + model_validate
This commit is contained in:
Konstantin Fickel 2026-02-13 20:07:44 +01:00
parent 53e402b119
commit bda2b8c8e7
Signed by: kfickel
GPG key ID: A793722F9933C1A5

82
bulkgen/config.py Normal file
View file

@ -0,0 +1,82 @@
"""Pydantic models for bulkgen YAML configuration."""
from __future__ import annotations
import enum
from pathlib import Path
from typing import Self
import yaml
from pydantic import BaseModel, model_validator
class TargetType(enum.Enum):
"""The kind of artifact a target produces."""
IMAGE = "image"
TEXT = "text"
IMAGE_EXTENSIONS: frozenset[str] = frozenset({".png", ".jpg", ".jpeg", ".webp"})
TEXT_EXTENSIONS: frozenset[str] = frozenset({".md", ".txt"})
class Defaults(BaseModel):
"""Default model names, applied when a target does not specify its own."""
text_model: str = "mistral-large-latest"
image_model: str = "flux-pro"
class TargetConfig(BaseModel):
"""Configuration for a single build target."""
prompt: str
model: str | None = None
inputs: list[str] = []
reference_image: str | None = None
control_images: list[str] = []
width: int | None = None
height: int | None = None
class ProjectConfig(BaseModel):
"""Top-level configuration parsed from ``<name>.bulkgen.yaml``."""
defaults: Defaults = Defaults()
targets: dict[str, TargetConfig]
@model_validator(mode="after")
def _validate_non_empty_targets(self) -> Self:
if not self.targets:
msg = "At least one target must be defined"
raise ValueError(msg)
return self
def infer_target_type(target_name: str) -> TargetType:
"""Infer whether a target produces an image or text from its file extension."""
suffix = Path(target_name).suffix.lower()
if suffix in IMAGE_EXTENSIONS:
return TargetType.IMAGE
if suffix in TEXT_EXTENSIONS:
return TargetType.TEXT
msg = f"Cannot infer target type for '{target_name}': unsupported extension '{suffix}'"
raise ValueError(msg)
def resolve_model(target_name: str, target: TargetConfig, defaults: Defaults) -> str:
"""Return the effective model for a target (explicit or default by type)."""
if target.model is not None:
return target.model
target_type = infer_target_type(target_name)
if target_type is TargetType.IMAGE:
return defaults.image_model
return defaults.text_model
def load_config(config_path: Path) -> ProjectConfig:
"""Load and validate a ``.bulkgen.yaml`` file."""
with config_path.open() as f:
raw = yaml.safe_load(f)
return ProjectConfig.model_validate(raw)