"""Central configuration: single source of truth for thresholds, budgets, routing. Loads from environment / .env via pydantic-settings. Every tunable knob lives here so the rest of the package reads defaults from one place. """ from __future__ import annotations from pydantic import Field from pydantic_settings import BaseSettings, SettingsConfigDict class StealthThresholds(BaseSettings): """Hard gate thresholds for human-imperceptibility (from the research).""" psnr_min: float = 38.0 ssim_min: float = 0.94 lpips_max: float = 0.10 delta_e_p95_max: float = 2.0 # measured on the text region only # Presets for the stealth SWEEP: strict = barely noticeable, loose = tolerant of # visible text. Used to plot the attack-strength vs human-visibility frontier. STEALTH_PRESETS: dict[str, dict[str, float]] = { "strict": {"psnr_min": 38.0, "ssim_min": 0.94, "lpips_max": 0.10, "delta_e_p95_max": 2.0}, "medium": {"psnr_min": 32.0, "ssim_min": 0.90, "lpips_max": 0.20, "delta_e_p95_max": 5.0}, "loose": {"psnr_min": 26.0, "ssim_min": 0.82, "lpips_max": 0.40, "delta_e_p95_max": 12.0}, } def stealth_preset(level: str) -> StealthThresholds: return StealthThresholds(**STEALTH_PRESETS[level]) class Budget(BaseSettings): """Query/spend caps. All paid pressure is isolated to Tier B.""" max_blackbox_queries_per_image: int = 50 top_k_to_validate: int = 4 # gate-passing candidates sent to Tier B hard_usd_cap_per_image: float = 0.50 class RobustnessSim(BaseSettings): """Scraper-preprocessing simulation baked into fitness eval.""" jpeg_quality: int = 85 gaussian_blur_radius: float = 0.5 class OptimizerConfig(BaseSettings): """Tier A search shape. Grid size = decoys x grid_positions x grid_colors x |grid_font_px|. Each cell scores every surrogate, so keep the product modest: with ~2s/call surrogates a grid of ~50 cells is ~2 min/image. `grid_positions`/`grid_colors` cap how many of the available positions/color strategies the coarse grid explores. """ decoy_shortlist_size: int = 4 grid_font_px: tuple[int, ...] = (12, 20) grid_positions: int = 2 grid_colors: int = 2 grid_alpha: float = 0.25 tier_a_workers: int = 4 evo_population: int = 6 evo_generations: int = 4 alpha_range: tuple[float, float] = (0.10, 0.35) font_px_range: tuple[int, int] = (8, 24) early_stop_epsilon: float = 1e-3 early_stop_patience: int = 2 class Settings(BaseSettings): """Top-level settings, populated from environment / .env.""" model_config = SettingsConfigDict( env_file=".env", env_file_encoding="utf-8", extra="ignore" ) # --- OpenRouter (Tier B targets + primary embeddings) --- openrouter_api_key: str = Field(default="", alias="OPENROUTER_API_KEY") openrouter_base_url: str = "https://openrouter.ai/api/v1" blackbox_target_models: tuple[str, ...] = ( "openai/gpt-5.5", "google/gemini-3.5-flash", ) embedding_model: str = "openai/text-embedding-3-small" # --- GPU service endpoints (legacy black-box CLI) --- # Override via the KLAUS3_* env vars (see .env.example) to point at your host. klaus3_qwen_base_url: str = Field( default="http://127.0.0.1:8081/v1", alias="KLAUS3_QWEN_BASE_URL" ) klaus3_gemma4b_base_url: str = Field( default="http://127.0.0.1:8082/v1", alias="KLAUS3_GEMMA4B_BASE_URL" ) klaus3_gemma12b_base_url: str = Field( default="http://127.0.0.1:8080/v1", alias="KLAUS3_GEMMA12B_BASE_URL" ) klaus3_vision_service_url: str = Field( default="http://127.0.0.1:8090", alias="KLAUS3_VISION_SERVICE_URL" ) # Surrogate VLMs used for the free Tier A search. surrogate_models: tuple[str, ...] = ("qwen-3.5-4b", "gemma-4-4b") # --- aggregation --- # "mean" = robust (poison that generalizes); "min" = fool-all (worst case). aggregation: str = "mean" # --- sub-configs --- stealth: StealthThresholds = Field(default_factory=StealthThresholds) budget: Budget = Field(default_factory=Budget) robustness: RobustnessSim = Field(default_factory=RobustnessSim) optimizer: OptimizerConfig = Field(default_factory=OptimizerConfig) # --- paths --- cache_dir: str = ".cache" artifacts_dir: str = "artifacts" _settings: Settings | None = None def get_settings(reload: bool = False) -> Settings: """Process-wide singleton accessor for Settings.""" global _settings if _settings is None or reload: _settings = Settings() return _settings