afa67's picture
deploy live demo
b252e6a verified
Raw
History Blame
5.05 kB
"""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
@lru_cache(maxsize=1)
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()