veil-pgd / src /veil_pgd /config.py
Klaus Clawd
Initial public release: VEIL-PGD v0.1
c793f45
Raw
History Blame Contribute Delete
4.6 kB
"""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