from __future__ import annotations import os from pathlib import Path from typing import Literal from urllib.parse import urlparse from pydantic import BaseModel, ConfigDict, Field, SecretStr, model_validator from .schema import ForestStyle class AppConfig(BaseModel): model_config = ConfigDict(extra="forbid") text_backend: Literal["demo", "hf_inference", "llama_cpp", "transformers", "modal"] = "demo" image_backend: Literal["demo", "flux", "hf_inference", "modal", "zerogpu"] = "demo" music_backend: Literal["none", "modal"] = "none" hf_text_model: str = "openbmb/MiniCPM4.1-8B" transformers_text_model: str = "openbmb/MiniCPM4.1-8B" hf_image_model: str = "black-forest-labs/FLUX.1-schnell" llama_base_url: str = "http://127.0.0.1:8080" llama_model: str = "compliment-forest-minicpm5-1b" flux_model_id: str = "black-forest-labs/FLUX.1-dev" flux_lora_id: str = "build-small-hackathon/compliment-forest-flux-lora" modal_text_endpoint: str | None = None modal_image_endpoint: str | None = None modal_music_endpoint: str | None = None modal_signing_key: SecretStr | None = None upstream_space_url: str | None = None local_files_only: bool = False default_seed: int = Field(default=3407, ge=0, le=2_147_483_647) default_style: ForestStyle = "surprise" trace_path: Path | None = None @model_validator(mode="after") def validate_local_text_server(self) -> AppConfig: if self.text_backend == "llama_cpp": hostname = urlparse(self.llama_base_url).hostname if hostname not in {"127.0.0.1", "localhost", "::1"}: raise ValueError("llama.cpp model server must be local") if self.text_backend == "modal": if not self.modal_text_endpoint or not self.modal_signing_key: raise ValueError("modal text backend requires endpoint credentials") if urlparse(self.modal_text_endpoint).scheme != "https": raise ValueError("modal text endpoint must use HTTPS") if self.image_backend == "modal": if not self.modal_image_endpoint or not self.modal_signing_key: raise ValueError("modal image backend requires endpoint credentials") if urlparse(self.modal_image_endpoint).scheme != "https": raise ValueError("modal image endpoint must use HTTPS") if self.music_backend == "modal": if not self.modal_music_endpoint or not self.modal_signing_key: raise ValueError("modal music backend requires endpoint credentials") if urlparse(self.modal_music_endpoint).scheme != "https": raise ValueError("modal music endpoint must use HTTPS") if self.upstream_space_url: parsed_upstream = urlparse(self.upstream_space_url) if parsed_upstream.scheme != "https" or not parsed_upstream.netloc: raise ValueError("upstream Space URL must use HTTPS") return self @classmethod def from_env(cls) -> AppConfig: trace_path = os.getenv("CF_TRACE_PATH") hosted_space = bool(os.getenv("SPACE_ID")) default_text_backend = "transformers" if hosted_space else "demo" default_image_backend = "zerogpu" if hosted_space else "demo" return cls( text_backend=os.getenv("CF_TEXT_BACKEND", default_text_backend), image_backend=os.getenv("CF_IMAGE_BACKEND", default_image_backend), music_backend=os.getenv("CF_MUSIC_BACKEND", "none"), hf_text_model=os.getenv( "CF_HF_TEXT_MODEL", "openbmb/MiniCPM4.1-8B", ), transformers_text_model=os.getenv( "CF_TRANSFORMERS_TEXT_MODEL", "openbmb/MiniCPM4.1-8B", ), hf_image_model=os.getenv( "CF_HF_IMAGE_MODEL", "black-forest-labs/FLUX.1-schnell", ), llama_base_url=os.getenv("CF_LLAMA_BASE_URL", "http://127.0.0.1:8080"), llama_model=os.getenv( "CF_LLAMA_MODEL", "compliment-forest-minicpm5-1b", ), flux_model_id=os.getenv( "CF_FLUX_MODEL_ID", "black-forest-labs/FLUX.1-dev", ), flux_lora_id=os.getenv( "CF_FLUX_LORA_ID", "build-small-hackathon/compliment-forest-flux-lora", ), modal_text_endpoint=os.getenv("CF_MODAL_TEXT_ENDPOINT"), modal_image_endpoint=os.getenv("CF_MODAL_IMAGE_ENDPOINT"), modal_music_endpoint=os.getenv("CF_MODAL_MUSIC_ENDPOINT"), modal_signing_key=(os.getenv("CF_MODAL_SIGNING_KEY") or os.getenv("HF_TOKEN")), upstream_space_url=os.getenv("CF_UPSTREAM_SPACE_URL"), local_files_only=os.getenv("CF_LOCAL_FILES_ONLY", "0") == "1", default_seed=int(os.getenv("CF_DEFAULT_SEED", "3407")), default_style=os.getenv("CF_DEFAULT_STYLE", "surprise"), trace_path=Path(trace_path) if trace_path else None, )