Spaces:
Runtime error
Runtime error
| """Typed configuration from .env / environment. | |
| Loading rules (see SPEC_NEW §5): | |
| - DATABASE_URL is required at startup — everything needs the DB. | |
| - External-service credentials are optional at load time so DRY_RUN and tests | |
| work without them, but any module that actually needs one must call | |
| `settings.require("NAME")`, which fails loudly with the exact missing var. | |
| """ | |
| from __future__ import annotations | |
| from functools import lru_cache | |
| from pathlib import Path | |
| from pydantic_settings import BaseSettings, SettingsConfigDict | |
| REPO_ROOT = Path(__file__).resolve().parent.parent | |
| MEDIA_DIR = REPO_ROOT / "media" | |
| FIXTURES_DIR = REPO_ROOT / "fixtures" | |
| class Settings(BaseSettings): | |
| model_config = SettingsConfigDict( | |
| env_file=REPO_ROOT / ".env", | |
| env_file_encoding="utf-8", | |
| extra="ignore", | |
| ) | |
| # Database (required) | |
| database_url: str | |
| # v2's tables live in this schema. Default "public"; set to e.g. "ugc" to share | |
| # an existing Supabase project without touching its other schemas (SPEC_NEW §3). | |
| db_schema: str = "public" | |
| # Supabase Storage — when set, avatar/product image uploads go to a public | |
| # bucket here instead of local disk (so everything lives in Supabase). | |
| supabase_url: str | None = None | |
| supabase_service_role_key: str | None = None | |
| supabase_bucket: str = "startframes" | |
| # Anthropic | |
| anthropic_api_key: str | None = None | |
| anthropic_model: str = "claude-sonnet-4-6" | |
| # Vertex AI / Veo | |
| google_cloud_project: str | None = None | |
| google_application_credentials: str | None = None | |
| vertex_location: str | None = None | |
| veo_model_id: str | None = None | |
| nano_banana_model: str | None = None # Nano Banana Pro (Gemini image) on Vertex — §16.3 | |
| # Windsor.ai (ad-metrics ingest — SPEC_NEW §7.6) | |
| windsor_api_key: str | None = None | |
| windsor_connector: str = "facebook" # configured connector (Meta now; TikTok later) | |
| windsor_fields: str | None = None # optional override; default = ad-level daily metrics | |
| # Meta (publish only — ingest is Windsor.ai above) | |
| meta_access_token: str | None = None | |
| meta_ad_account_id: str | None = None | |
| meta_api_version: str | None = None | |
| meta_test_campaign_id: str | None = None | |
| meta_page_id: str | None = None | |
| # Behavior / guardrails | |
| dry_run: bool = True | |
| max_generations_per_day: int = 40 | |
| qc_pass_threshold: float = 0.85 | |
| qc_flag_threshold: float = 0.70 | |
| max_segment_attempts: int = 3 | |
| # Video engine (SPEC_NEW §11) | |
| veo_resolution: str = "720p" # all clips 720p, exactly like the MVP | |
| seed_takes: int = 2 # hook seed candidates to generate (§11.3) | |
| seed_autopick: bool = True # auto-pick best hook take via QC; human override | |
| veo_rai_autoretry: bool = False # OFF = surface safety blocks (v1 default, §12.5) | |
| veo_rai_rephrase: bool = True # LEGACY — auto-retry now swaps the SEED, not words (§12.5) | |
| veo_rai_max_retries: int = 3 # auto rephrase+retry loop length per safety block (§12.5) | |
| # Speaking-pace model for dynamic duration (§11.1). These MIRROR the defaults | |
| # baked into pipeline/duration.py; re-pacing is a deliberate change to both. | |
| words_per_second: float = 2.3 | |
| tail_budget_s: float = 0.7 | |
| # B-roll (Seedance, SPEC_NEW §15) | |
| seedance_provider: str = "volcengine" # fal | replicate | volcengine (owner uses Volcengine Ark) | |
| seedance_api_key: str | None = None # Volcengine Ark API key (ARK_API_KEY) | |
| seedance_model: str = "doubao-seedance-1-0-lite-i2v-250428" # verify in your Ark console | |
| seedance_base_url: str = "https://ark.cn-beijing.volces.com/api/v3" | |
| # Assembly (SPEC_NEW §11.4) | |
| loudnorm_target_i: float = -14.0 # integrated LUFS | |
| caption_max_words_per_line: int = 4 # 2–4 words/line, burned ASS | |
| captions_enabled: bool = True # best-effort; skipped if ASR unavailable | |
| # QC / ASR | |
| kb_whisper_model: str = "KBLab/kb-whisper-large" | |
| kb_whisper_fallback_model: str = "KBLab/kb-whisper-medium" | |
| kb_whisper_device: str = "auto" | |
| kb_whisper_compute_type: str = "" | |
| # Rules | |
| target_cpa: float = 300.0 | |
| # Approval UI | |
| approval_ui_user: str = "admin" | |
| approval_ui_password: str | None = None | |
| def require(self, name: str) -> str: | |
| """Return a config value, failing loudly if it is unset/empty.""" | |
| value = getattr(self, name.lower(), None) | |
| if value is None or (isinstance(value, str) and not value.strip()): | |
| raise RuntimeError( | |
| f"Missing required configuration: {name.upper()}. " | |
| f"Set it in .env (see .env.example)." | |
| ) | |
| return value | |
| def get_settings() -> Settings: | |
| return Settings() # type: ignore[call-arg] # database_url comes from env | |
| def reset_settings_cache() -> None: | |
| """Test helper: force re-read of the environment.""" | |
| get_settings.cache_clear() | |