Files
dreamweaver/backend/app/core/config.py

186 lines
6.8 KiB
Python

from pydantic import Field, model_validator
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
"""应用全局配置"""
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
extra="ignore",
)
# 应用基础配置
app_name: str = "DreamWeaver"
debug: bool = False
secret_key: str = Field(..., description="JWT 签名密钥")
base_url: str = Field("http://localhost:8000", description="后端对外回调地址")
# 数据库
database_url: str = Field(..., description="SQLAlchemy async URL")
# OAuth - GitHub
github_client_id: str = ""
github_client_secret: str = ""
# OAuth - Google
google_client_id: str = ""
google_client_secret: str = ""
# AI Capability Keys
text_api_key: str = ""
tts_api_base: str = ""
tts_api_key: str = ""
image_api_key: str = ""
# Additional Provider API Keys
openai_api_key: str = ""
elevenlabs_api_key: str = ""
cqtai_api_key: str = ""
minimax_api_key: str = ""
minimax_group_id: str = ""
antigravity_api_key: str = ""
antigravity_api_base: str = ""
# AI Model Configuration
text_model: str = "gemini-2.0-flash"
openai_model: str = "gpt-4o-mini"
tts_model: str = ""
image_model: str = "nano-banana-pro"
tts_minimax_model: str = "speech-2.6-turbo"
tts_elevenlabs_model: str = "eleven_multilingual_v2"
tts_edge_voice: str = "zh-CN-XiaoxiaoNeural"
antigravity_model: str = "gemini-3-pro-image"
# Provider routing (ordered lists)
text_providers: list[str] = Field(default_factory=lambda: ["gemini"])
image_providers: list[str] = Field(default_factory=lambda: ["cqtai"])
tts_providers: list[str] = Field(default_factory=lambda: ["minimax", "elevenlabs", "edge_tts"])
storybook_providers: list[str] = Field(default_factory=lambda: ["storybook_primary"])
enable_demo_providers: bool = Field(
False,
description="Enable local deterministic demo providers for portfolio demos",
)
story_audio_cache_dir: str = Field(
"storage/audio",
description="Directory for cached story audio files",
)
voice_session_storage_dir: str = Field(
"storage/voice_sessions",
description="Directory for persisted voice co-creation session assets",
)
voice_transcription_mode: str = Field(
"demo",
description="Voice transcription mode: demo, openai, or disabled",
)
voice_transcription_model: str = Field(
"gpt-4o-mini-transcribe",
description="Model used when voice transcription mode is OpenAI-backed",
)
voice_transcription_language: str = Field(
"zh",
description="Preferred language hint for voice transcription",
)
voice_turn_max_upload_bytes: int = Field(
5 * 1024 * 1024,
description="Maximum accepted upload size in bytes for one voice turn audio file",
)
voice_session_default_list_limit: int = Field(
8,
description="Default number of recent voice sessions returned to the client",
)
voice_session_max_list_limit: int = Field(
20,
description="Maximum number of recent voice sessions returned to the client",
)
story_audio_cache_ttl_days: int = Field(
30,
description="TTL in days before cached story audio is pruned",
)
generation_job_stale_minutes: int = Field(
60,
description="Minutes before a running generation job is considered stale",
)
# Celery (Redis)
celery_broker_url: str = Field("redis://localhost:6379/0")
celery_result_backend: str = Field("redis://localhost:6379/0")
# Generic Redis
redis_url: str = Field("redis://localhost:6379/0", description="Redis connection URL")
redis_sentinel_enabled: bool = Field(False, description="Whether to enable Redis Sentinel")
redis_sentinel_nodes: str = Field(
"",
description="Comma-separated Redis Sentinel nodes, e.g. host1:26379,host2:26379",
)
redis_sentinel_master_name: str = Field("mymaster", description="Redis Sentinel master name")
redis_sentinel_password: str = Field("", description="Password for Redis Sentinel (optional)")
redis_sentinel_db: int = Field(0, description="Redis DB index when using Sentinel")
redis_sentinel_socket_timeout: float = Field(
0.5,
description="Socket timeout in seconds for Sentinel clients",
)
# Admin console
enable_admin_console: bool = False
admin_username: str = "admin"
admin_password: str = ""
# CORS
cors_origins: list[str] = Field(default_factory=lambda: ["http://localhost:5173"])
@model_validator(mode="after")
def _require_core_settings(self) -> "Settings": # type: ignore[override]
missing = []
if not self.secret_key or self.secret_key == "change-me-in-production":
missing.append("SECRET_KEY")
if not self.database_url:
missing.append("DATABASE_URL")
if self.redis_sentinel_enabled and not self.redis_sentinel_nodes.strip():
missing.append("REDIS_SENTINEL_NODES")
if missing:
raise ValueError(f"Missing required settings: {', '.join(missing)}")
if self.enable_admin_console:
weak_admin_passwords = {"", "admin", "admin123", "password", "change-me"}
if not self.debug and self.admin_password in weak_admin_passwords:
raise ValueError(
"ADMIN_PASSWORD must be set to a strong value when admin console is enabled"
)
return self
@property
def redis_sentinel_hosts(self) -> list[tuple[str, int]]:
"""Parse Redis Sentinel nodes into (host, port) tuples."""
nodes = []
raw = self.redis_sentinel_nodes.strip()
if not raw:
return nodes
for item in raw.split(","):
value = item.strip()
if not value:
continue
if ":" not in value:
raise ValueError(f"Invalid sentinel node format: {value}")
host, port_text = value.rsplit(":", 1)
if not host:
raise ValueError(f"Invalid sentinel node host: {value}")
try:
port = int(port_text)
except ValueError as exc:
raise ValueError(f"Invalid sentinel node port: {value}") from exc
nodes.append((host, port))
return nodes
@property
def redis_sentinel_urls(self) -> list[str]:
"""Build Celery-compatible Sentinel URLs with DB index."""
return [
f"sentinel://{host}:{port}/{self.redis_sentinel_db}"
for host, port in self.redis_sentinel_hosts
]
settings = Settings()