from __future__ import annotations from typing import Any, Literal, Protocol from pydantic import BaseModel, ConfigDict, Field class ChatTemplateTokenizer(Protocol): def apply_chat_template( self, messages: list[dict[str, str]], *, tokenize: bool, add_generation_prompt: bool, enable_thinking: bool, ) -> str: ... class TextTrainingConfig(BaseModel): model_config = ConfigDict(extra="forbid", frozen=True) base_model: str = "openbmb/MiniCPM5-1B" dataset_id: str = "build-small-hackathon/compliment-forest-sft" model_id: str = "build-small-hackathon/compliment-forest-minicpm5-1b" adapter_id: str = "build-small-hackathon/compliment-forest-minicpm5-1b-lora" output_dir: str = "/training/compliment-forest-text" gguf_f16_name: str = "compliment-forest-minicpm5-1b.F16.gguf" gguf_q4_name: str = "compliment-forest-minicpm5-1b.Q4_K_M.gguf" max_length: int = Field(default=2048, ge=256) epochs: int = Field(default=2, ge=1) max_steps: int = -1 train_limit: int | None = None validation_limit: int | None = None learning_rate: float = 2e-4 per_device_batch_size: int = 2 gradient_accumulation_steps: int = 8 lora_rank: int = 16 lora_alpha: int = 32 lora_dropout: float = 0.05 seed: int = 3407 target_modules: tuple[str, ...] = ( "q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj", ) @classmethod def smoke(cls) -> TextTrainingConfig: return cls( epochs=1, max_steps=2, train_limit=16, validation_limit=8, ) class FluxTrainingConfig(BaseModel): model_config = ConfigDict(extra="forbid", frozen=True) base_model: str = "black-forest-labs/FLUX.1-dev" dataset_id: str = "build-small-hackathon/compliment-forest-watercolor" model_id: str = "build-small-hackathon/compliment-forest-flux-lora" trigger_token: str = "cmprst_forest" output_dir: str = "/training/compliment-forest-flux" resolution: int = 512 max_train_steps: int = 500 train_batch_size: int = 1 gradient_accumulation_steps: int = 1 learning_rate: float = 1e-4 rank: int = 16 lora_alpha: int = 16 repeats: int = 4 seed: int = 3407 validation_prompt: str = ( "a brave snail crossing a fern at dawn, cmprst_forest, soft watercolor " "storybook, gentle washes, warm dappled light, paper texture, kind expression, " "lots of negative space" ) @classmethod def smoke(cls) -> FluxTrainingConfig: return cls( max_train_steps=2, repeats=1, ) TrainedForestStyle = Literal[ "watercolor", "paper_cut", "moonlit_gouache", "botanical_ink", ] _STYLE_TRAINING = { "watercolor": { "trigger": "cmprst_watercolor", "repo_suffix": "watercolor", "validation": "a gentle fox pausing beside ferns at dawn", }, "paper_cut": { "trigger": "cmprst_papercut", "repo_suffix": "paper-cut", "validation": "a thoughtful badger beside layered woodland leaves", }, "moonlit_gouache": { "trigger": "cmprst_moonlit", "repo_suffix": "moonlit-gouache", "validation": "a small owl resting in a moonlit pine clearing", }, "botanical_ink": { "trigger": "cmprst_inkwash", "repo_suffix": "botanical-ink", "validation": "a patient hare beneath sparse woodland flowers", }, } class FluxStyleTrainingConfig(BaseModel): model_config = ConfigDict(extra="forbid", frozen=True) style: TrainedForestStyle base_model: str = "black-forest-labs/FLUX.1-schnell" dataset_id: str = "thangvip/compliment-forest-multistyle-v2" dataset_config_name: str model_id: str trigger_token: str output_dir: str resolution: int = 512 max_train_steps: int = 300 train_batch_size: int = 1 gradient_accumulation_steps: int = 1 learning_rate: float = 1e-4 rank: int = 16 lora_alpha: int = 16 repeats: int = 3 seed: int = 3407 guidance_scale: float = 0 validation_prompt: str @classmethod def for_style( cls, style: TrainedForestStyle, *, smoke: bool = False, ) -> FluxStyleTrainingConfig: spec = _STYLE_TRAINING[style] config = cls( style=style, dataset_config_name=style, model_id=( f"thangvip/compliment-forest-{spec['repo_suffix']}-flux-lora-v2" ), trigger_token=spec["trigger"], output_dir=f"/training/compliment-forest-{spec['repo_suffix']}-flux", validation_prompt=f"{spec['trigger']}, {spec['validation']}", ) if smoke: return config.model_copy(update={"max_train_steps": 2, "repeats": 1}) return config def format_training_example( example: dict[str, Any], tokenizer: ChatTemplateTokenizer, ) -> dict[str, str]: messages = example.get("messages") if not isinstance(messages, list): raise ValueError("training example must contain a messages list") return { "text": tokenizer.apply_chat_template( messages, tokenize=False, add_generation_prompt=False, enable_thinking=False, ) }