Files
hack/tools/scripts/llm-key-hunter/results/_cache/content/arelaxedscholar_animus.json
T

1 line
171 KiB
JSON

{"repo": "arelaxedscholar/animus", "files": {"src/config/settings.rs": {"68c33cb63eb155af316744db63c6a5d0a26dc23a": {"t": 1785745871.8109531, "n": 20917, "h": "f5440ca45223173a", "c": "//! Application settings loaded from environment variables\n\nuse serde::Deserialize;\n\n/// Root configuration structure\n#[derive(Debug, Clone, Deserialize)]\npub struct Settings {\n pub database: DatabaseConfig,\n pub s3: S3Config,\n pub llm: LlmConfig,\n pub tts: TtsConfig,\n pub assets: AssetConfig,\n pub youtube: YouTubeConfig,\n pub channel: ChannelConfig,\n pub content_strategy: ContentStrategyConfig,\n pub script_improvement: ScriptImprovementConfig,\n pub fact_checker: FactCheckerConfig,\n pub notifications: NotificationConfig,\n pub control_api_port: u16,\n}\n\nuse crate::nodes::tts::TTSProvider;\n\n#[derive(Debug, Clone, Deserialize)]\npub struct DatabaseConfig {\n pub url: String,\n}\n\n#[derive(Debug, Clone, Deserialize)]\npub struct S3Config {\n pub endpoint: String,\n pub access_key: String,\n pub secret_key: String,\n pub bucket: String,\n pub region: String,\n}\n\n#[derive(Debug, Clone, Deserialize)]\npub struct LlmConfig {\n pub deepseek_api_key: String,\n pub deepseek_base_url: String,\n pub gemini_api_key: String,\n /// Which provider to use by default: \"deepseek\" or \"gemini\"\n pub default_provider: String,\n}\n\n#[derive(Debug, Clone, Deserialize)]\npub struct TtsConfig {\n /// TTS provider: elevenlabs, qwen3, openai, coqui, piper\n pub provider: TTSProvider,\n\n // ElevenLabs settings\n pub elevenlabs_api_key: Option<String>,\n pub elevenlabs_voice_id: Option<String>,\n pub elevenlabs_model_id: Option<String>,\n\n // Qwen3-TTS settings (self-hosted, OpenAI-compatible)\n pub qwen3_api_url: Option<String>,\n pub qwen3_api_key: Option<String>,\n pub qwen3_voice: Option<String>,\n\n // OpenAI TTS settings\n pub openai_api_key: Option<String>,\n pub openai_voice: Option<String>,\n pub openai_model: Option<String>,\n\n // Local TTS settings (Coqui/Piper)\n pub local_model_path: Option<String>,\n pub local_speaker_id: Option<String>,\n\n // Common settings\n pub stability: f32,\n pub similarity_boost: f32,\n pub speed: f32,\n}\n\n#[derive(Debug, Clone, Deserialize)]\npub struct YouTubeConfig {\n pub client_id: String,\n pub client_secret: String,\n pub refresh_token: String,\n /// Default YouTube account ID to use (optional, for single-account setups)\n pub default_account_id: Option<i32>,\n}\n\n#[derive(Debug, Clone, Deserialize)]\npub struct ChannelConfig {\n pub name: String,\n pub tagline: String,\n}\n\n#[derive(Debug, Clone, Deserialize)]\npub struct ContentStrategyConfig {\n pub target_duration_min: u32,\n pub target_duration_max: u32,\n pub videos_per_week: u32,\n}\n\n#[derive(Debug, Clone, Deserialize)]\npub struct NotificationConfig {\n pub discord_webhook_url: Option<String>,\n pub smtp_host: Option<String>,\n pub smtp_port: Option<u16>,\n pub smtp_user: Option<String>,\n pub smtp_pass: Option<String>,\n pub notification_email: Option<String>,\n}\n\n/// Pexels and AI asset services config\n#[derive(Debug, Clone, Deserialize)]\npub struct AssetConfig {\n pub pexels_api_key: String,\n pub leonardo_api_key: Option<String>,\n pub freesound_api_key: Option<String>,\n pub sd_api_url: Option<String>,\n pub sd_api_key: Option<String>,\n pub min_clips_per_section: u32,\n /// Maximum retry attempts for downloading videos\n pub max_retries: u32,\n /// Minimum file size in KB to consider video valid\n pub min_file_size_kb: u64,\n /// Whether to validate videos with ffprobe (if available)\n pub validate_with_ffprobe: bool,\n /// Whether to fallback to AI images when videos fail\n pub fallback_to_images: bool,\n}\n\n#[derive(Debug, Clone, Deserialize)]\npub struct FactCheckerConfig {\n pub enabled: bool,\n pub groq_api_key: String,\n pub strict_mode: bool,\n pub verify_wisdom_quotes: bool,\n pub verify_historical: bool,\n pub verify_general: bool,\n pub rewrite_trigger_threshold: f64,\n pub max_claims_per_script: u32,\n pub fail_open: bool,\n}\n\n/// Script self-improvement loop configuration\n#[derive(Debug, Clone, Deserialize)]\npub struct ScriptImprovementConfig {\n /// Enable the self-improvement loop\n pub enabled: bool,\n /// Number of initial script candidates to generate in parallel\n pub candidate_count: u32,\n /// Minimum score (1.0-10.0) to accept without refinement\n pub quality_threshold: f32,\n /// Maximum refinement iterations before accepting best available\n pub max_iterations: u32,\n /// Number of iterations without improvement before giving up early\n pub stagnation_threshold: u32,\n /// Total timeout for the entire self-improvement process (seconds)\n pub timeout_seconds: u32,\n}\n\nimpl Settings {\n /// Load settings from environment variables\n #[allow(unsafe_code)]\n pub fn from_env() -> Result<Self, config::ConfigError> {\n #[cfg(not(test))]\n dotenvy::dotenv().ok();\n\n let settings = config::Config::builder()\n // Database\n .set_default(\"database.url\", \"\")?\n .set_override_option(\"database.url\", std::env::var(\"DATABASE_URL\").ok())?\n // S3\n .set_default(\"s3.endpoint\", \"http://localhost:9000\")?\n .set_override_option(\"s3.endpoint\", std::env::var(\"S3_ENDPOINT\").ok())?\n .set_default(\"s3.access_key\", \"minioadmin\")?\n .set_override_option(\"s3.access_key\", std::env::var(\"S3_ACCESS_KEY\").ok())?\n .set_default(\"s3.secret_key\", \"minioadmin\")?\n .set_override_option(\"s3.secret_key\", std::env::var(\"S3_SECRET_KEY\").ok())?\n .set_default(\"s3.bucket\", \"animus-assets\")?\n .set_override_option(\"s3.bucket\", std::env::var(\"S3_BUCKET\").ok())?\n .set_default(\"s3.region\", \"us-east-1\")?\n .set_override_option(\"s3.region\", std::env::var(\"S3_REGION\").ok())?\n // LLM\n .set_default(\"llm.deepseek_api_key\", \"\")?\n .set_override_option(\n \"llm.deepseek_api_key\",\n std::env::var(\"DEEPSEEK_API_KEY\").ok(),\n )?\n .set_default(\"llm.deepseek_base_url\", \"https://api.deepseek.com\")?\n .set_override_option(\n \"llm.deepseek_base_url\",\n std::env::var(\"DEEPSEEK_BASE_URL\").ok(),\n )?\n .set_default(\"llm.gemini_api_key\", \"\")?\n .set_override_option(\"llm.gemini_api_key\", std::env::var(\"GEMINI_API_KEY\").ok())?\n .set_default(\"llm.default_provider\", \"deepseek\")?\n .set_override_option(\n \"llm.default_provider\",\n std::env::var(\"LLM_DEFAULT_PROVIDER\").ok(),\n )?\n // TTS\n .set_default(\"tts.provider\", \"elevenlabs\")?\n .set_override_option(\"tts.provider\", std::env::var(\"TTS_PROVIDER\").ok())?\n // ElevenLabs\n .set_default(\"tts.elevenlabs_api_key\", Option::<String>::None)?\n .set_override_option(\n \"tts.elevenlabs_api_key\",\n std::env::var(\"ELEVENLABS_API_KEY\").ok(),\n )?\n .set_default(\"tts.elevenlabs_voice_id\", Option::<String>::None)?\n .set_override_option(\n \"tts.elevenlabs_voice_id\",\n std::env::var(\"ELEVENLABS_VOICE_ID\").ok(),\n )?\n .set_default(\"tts.elevenlabs_model_id\", Option::<String>::None)?\n .set_override_option(\n \"tts.elevenlabs_model_id\",\n std::env::var(\"ELEVENLABS_MODEL_ID\").ok(),\n )?\n // Qwen3-TTS\n .set_default(\"tts.qwen3_api_url\", Option::<String>::None)?\n .set_override_option(\"tts.qwen3_api_url\", std::env::var(\"QWEN3_API_URL\").ok())?\n .set_default(\"tts.qwen3_api_key\", Option::<String>::None)?\n .set_override_option(\"tts.qwen3_api_key\", std::env::var(\"QWEN3_API_KEY\").ok())?\n .set_default(\"tts.qwen3_voice\", Option::<String>::None)?\n .set_override_option(\"tts.qwen3_voice\", std::env::var(\"QWEN3_VOICE\").ok())?\n // OpenAI TTS\n .set_default(\"tts.openai_api_key\", Option::<String>::None)?\n .set_override_option(\n \"tts.openai_api_key\",\n std::env::var(\"OPENAI_TTS_API_KEY\").ok(),\n )?\n .set_default(\"tts.openai_voice\", Option::<String>::None)?\n .set_override_option(\"tts.openai_voice\", std::env::var(\"OPENAI_TTS_VOICE\").ok())?\n .set_default(\"tts.openai_model\", Option::<String>::None)?\n .set_override_option(\"tts.openai_model\", std::env::var(\"OPENAI_TTS_MODEL\").ok())?\n // Local TTS\n .set_default(\"tts.local_model_path\", Option::<String>::None)?\n .set_override_option(\n \"tts.local_model_path\",\n std::env::var(\"LOCAL_TTS_MODEL_PATH\").ok(),\n )?\n .set_default(\"tts.local_speaker_id\", Option::<String>::None)?\n .set_override_option(\n \"tts.local_speaker_id\",\n std::env::var(\"LOCAL_TTS_SPEAKER_ID\").ok(),\n )?\n // Common TTS settings\n .set_default(\"tts.stability\", 0.5)?\n .set_default(\"tts.similarity_boost\", 0.75)?\n .set_default(\"tts.speed\", 1.0)?\n .set_override_option(\n \"tts.speed\",\n std::env::var(\"TTS_SPEED\")\n .ok()\n .and_then(|v| v.parse::<f64>().ok()),\n )?\n // Assets (Pexels, AI services, sound effects)\n .set_default(\"assets.pexels_api_key\", \"\")?\n .set_override_option(\n \"assets.pexels_api_key\",\n std::env::var(\"PEXELS_API_KEY\").ok(),\n )?\n .set_default(\"assets.leonardo_api_key\", Option::<String>::None)?\n .set_override_option(\n \"assets.leonardo_api_key\",\n std::env::var(\"LEONARDO_API_KEY\")\n .or_else(|_| std::env::var(\"LEONARDOAI_API_KEY\"))\n .ok()\n .filter(|s| !s.is_empty()),\n )?\n .set_default(\"assets.freesound_api_key\", Option::<String>::None)?\n .set_override_option(\n \"assets.freesound_api_key\",\n std::env::var(\"FREESOUND_API_KEY\")\n .ok()\n .filter(|s| !s.is_empty()),\n )?\n .set_default(\"assets.sd_api_url\", Option::<String>::None)?\n .set_override_option(\n \"assets.sd_api_url\",\n std::env::var(\"SD_API_URL\").ok().filter(|s| !s.is_empty()),\n )?\n .set_default(\"assets.sd_api_key\", Option::<String>::None)?\n .set_override_option(\n \"assets.sd_api_key\",\n std::env::var(\"SD_API_KEY\").ok().filter(|s| !s.is_empty()),\n )?\n .set_default(\"assets.min_clips_per_section\", 3)?\n .set_override_option(\n \"assets.min_clips_per_section\",\n std::env::var(\"ASSET_MIN_CLIPS_PER_SECTION\")\n .ok()\n .and_then(|v| v.parse::<i64>().ok()),\n )?\n .set_default(\"assets.max_retries\", 3)?\n .set_override_option(\n \"assets.max_retries\",\n std::env::var(\"ASSET_MAX_RETRIES\")\n .ok()\n .and_then(|v| v.parse::<i64>().ok()),\n )?\n .set_default(\"assets.min_file_size_kb\", 100)?\n .set_override_option(\n \"assets.min_file_size_kb\",\n std::env::var(\"ASSET_MIN_FILE_SIZE_KB\")\n .ok()\n .and_then(|v| v.parse::<i64>().ok()),\n )?\n .set_default(\"assets.validate_with_ffprobe\", true)?\n .set_override_option(\n \"assets.validate_with_ffprobe\",\n std::env::var(\"ASSET_VALIDATE_WITH_FFPROBE\")\n .ok()\n .map(|v| v.to_lowercase() == \"true\" || v == \"1\"),\n )?\n .set_default(\"assets.fallback_to_images\", true)?\n .set_override_option(\n \"assets.fallback_to_images\",\n std::env::var(\"ASSET_FALLBACK_TO_IMAGES\")\n .ok()\n .map(|v| v.to_lowercase() == \"true\" || v == \"1\"),\n )?\n // YouTube\n .set_default(\"youtube.client_id\", \"\")?\n .set_override_option(\"youtube.client_id\", std::env::var(\"YOUTUBE_CLIENT_ID\").ok())?\n .set_default(\"youtube.client_secret\", \"\")?\n .set_override_option(\n \"youtube.client_secret\",\n std::env::var(\"YOUTUBE_CLIENT_SECRET\").ok(),\n )?\n .set_default(\"youtube.refresh_token\", \"\")?\n .set_override_option(\n \"youtube.refresh_token\",\n std::env::var(\"YOUTUBE_REFRESH_TOKEN\").ok(),\n )?\n .set_default(\"youtube.default_account_id\", Option::<i32>::None)?\n .set_override_option(\n \"youtube.default_account_id\",\n std::env::var(\"YOUTUBE_DEFAULT_ACCOUNT_ID\")\n .ok()\n .and_then(|v| v.parse::<i32>().ok()),\n )?\n // Channel\n .set_default(\"channel.name\", \"Excelsior Academy\")?\n .set_override_option(\"channel.name\", std::env::var(\"CHANNEL_NAME\").ok())?\n .set_default(\"channel.tagline\", \"Wisdom for the journey upward\")?\n .set_override_option(\"channel.tagline\", std::env::var(\"CHANNEL_TAGLINE\").ok())?\n // Content Strategy\n .set_default(\"content_strategy.target_duration_min\", 12)?\n .set_override_option(\n \"content_strategy.target_duration_min\",\n std::env::var(\"TARGET_DURATION_MIN\")\n .ok()\n .and_then(|v| v.parse::<i64>().ok()),\n )?\n .set_default(\"content_strategy.target_duration_max\", 20)?\n .set_override_option(\n \"content_strategy.target_duration_max\",\n std::env::var(\"TARGET_DURATION_MAX\")\n .ok()\n .and_then(|v| v.parse::<i64>().ok()),\n )?\n .set_default(\"content_strategy.videos_per_week\", 4)?\n .set_override_option(\n \"content_strategy.videos_per_week\",\n std::env::var(\"VIDEOS_PER_WEEK\")\n .ok()\n .and_then(|v| v.parse::<i64>().ok()),\n )?\n // Script Self-Improvement Loop\n .set_default(\"script_improvement.enabled\", true)?\n .set_override_option(\n \"script_improvement.enabled\",\n std::env::var(\"SCRIPT_IMPROVEMENT_ENABLED\")\n .ok()\n .map(|v| v.to_lowercase() == \"true\" || v == \"1\"),\n )?\n .set_default(\"script_improvement.candidate_count\", 3)?\n .set_override_option(\n \"script_improvement.candidate_count\",\n std::env::var(\"SCRIPT_IMPROVEMENT_CANDIDATES\")\n .ok()\n .and_then(|v| v.parse::<i64>().ok()),\n )?\n .set_default(\"script_improvement.quality_threshold\", 9.0)?\n .set_override_option(\n \"script_improvement.quality_threshold\",\n std::env::var(\"SCRIPT_IMPROVEMENT_THRESHOLD\")\n .ok()\n .and_then(|v| v.parse::<f64>().ok()),\n )?\n .set_default(\"script_improvement.max_iterations\", 10)?\n .set_override_option(\n \"script_improvement.max_iterations\",\n std::env::var(\"SCRIPT_IMPROVEMENT_MAX_ITERATIONS\")\n .ok()\n .and_then(|v| v.parse::<i64>().ok()),\n )?\n .set_default(\"script_improvement.stagnation_threshold\", 3)?\n .set_override_option(\n \"script_improvement.stagnation_threshold\",\n std::env::var(\"SCRIPT_IMPROVEMENT_STAGNATION_THRESHOLD\")\n .ok()\n .and_then(|v| v.parse::<i64>().ok()),\n )?\n .set_default(\"script_improvement.timeout_seconds\", 1800)?\n .set_override_option(\n \"script_improvement.timeout_seconds\",\n std::env::var(\"SCRIPT_IMPROVEMENT_TIMEOUT\")\n .ok()\n .and_then(|v| v.parse::<i64>().ok()),\n )?\n // Fact Checker\n .set_default(\"fact_checker.enabled\", true)?\n .set_override_option(\n \"fact_checker.enabled\",\n std::env::var(\"FACT_CHECKER_ENABLED\")\n .ok()\n .map(|v| v.to_lowercase() == \"true\" || v == \"1\"),\n )?\n .set_default(\"fact_checker.groq_api_key\", \"\")?\n .set_override_option(\"fact_checker.groq_api_key\", std::env::var(\"GROQ_API_KEY\").ok())?\n .set_default(\"fact_checker.strict_mode\", false)?\n .set_override_option(\n \"fact_checker.strict_mode\",\n std::env::var(\"FACT_CHECKER_STRICT_MODE\")\n .ok()\n .map(|v| v.to_lowercase() == \"true\" || v == \"1\"),\n )?\n .set_default(\"fact_checker.verify_wisdom_quotes\", true)?\n .set_default(\"fact_checker.verify_historical\", true)?\n .set_default(\"fact_checker.verify_general\", true)?\n .set_default(\"fact_checker.rewrite_trigger_threshold\", 0.2)?\n .set_default(\"fact_checker.max_claims_per_script\", 20)?\n .set_default(\"fact_checker.fail_open\", true)?\n // Notifications\n .set_default(\"notifications.discord_webhook_url\", Option::<String>::None)?\n .set_override_option(\n \"notifications.discord_webhook_url\",\n std::env::var(\"DISCORD_WEBHOOK_URL\").ok(),\n )?\n .set_default(\"notifications.smtp_host\", Option::<String>::None)?\n .set_override_option(\"notifications.smtp_host\", std::env::var(\"SMTP_HOST\").ok())?\n .set_default(\"notifications.smtp_port\", Option::<u16>::None)?\n .set_override_option(\n \"notifications.smtp_port\",\n std::env::var(\"SMTP_PORT\")\n .ok()\n .and_then(|v| v.parse::<i64>().ok()),\n )?\n .set_default(\"notifications.smtp_user\", Option::<String>::None)?\n .set_override_option(\"notifications.smtp_user\", std::env::var(\"SMTP_USER\").ok())?\n .set_default(\"notifications.smtp_pass\", Option::<String>::None)?\n .set_override_option(\"notifications.smtp_pass\", std::env::var(\"SMTP_PASS\").ok())?\n .set_default(\"notifications.notification_email\", Option::<String>::None)?\n .set_override_option(\n \"notifications.notification_email\",\n std::env::var(\"NOTIFICATION_EMAIL\").ok(),\n )?\n // Control API\n .set_default(\"control_api_port\", 8080)?\n .set_override_option(\n \"control_api_port\",\n std::env::var(\"CONTROL_API_PORT\")\n .ok()\n .and_then(|v| v.parse::<i64>().ok()),\n )?\n .build()?;\n\n settings.try_deserialize()\n }\n}\n\n#[cfg(test)]\nmod tests {\n #![allow(unsafe_code)]\n use super::*;\n\n #[test]\n fn test_default_settings() {\n // This will use defaults since no .env is loaded in tests\n std::env::set_var(\"DATABASE_URL\", \"postgres://test:test@localhost/test\");\n let settings = Settings::from_env();\n assert!(settings.is_ok());\n }\n\n #[test]\n fn test_asset_config_loading() {\n std::env::set_var(\"DATABASE_URL\", \"postgres://test:test@localhost/test\");\n std::env::set_var(\"LEONARDO_API_KEY\", \"test_leonardo_key\");\n std::env::set_var(\"FREESOUND_API_KEY\", \"test_freesound_key\");\n std::env::set_var(\"ASSET_MIN_CLIPS_PER_SECTION\", \"5\");\n\n let settings = Settings::from_env().unwrap();\n assert_eq!(\n settings.assets.leonardo_api_key,\n Some(\"test_leonardo_key\".to_string())\n );\n assert_eq!(\n settings.assets.freesound_api_key,\n Some(\"test_freesound_key\".to_string())\n );\n assert_eq!(settings.assets.min_clips_per_section, 5);\n\n // Test backward compatibility with LEONARDOAI_API_KEY\n std::env::remove_var(\"LEONARDO_API_KEY\");\n std::env::set_var(\"LEONARDOAI_API_KEY\", \"test_leonardoai_key\");\n let settings2 = Settings::from_env().unwrap();\n assert_eq!(\n settings2.assets.leonardo_api_key,\n Some(\"test_leonardoai_key\".to_string())\n );\n\n // Clean up\n std::env::remove_var(\"LEONARDOAI_API_KEY\");\n std::env::remove_var(\"FREESOUND_API_KEY\");\n std::env::remove_var(\"ASSET_MIN_CLIPS_PER_SECTION\");\n }\n}\n"}, "d0925888edbf3f6c90d79301ea5227b1f0065e09": {"t": 1785757447.708342, "n": 20917, "h": "f5440ca45223173a", "c": "//! Application settings loaded from environment variables\n\nuse serde::Deserialize;\n\n/// Root configuration structure\n#[derive(Debug, Clone, Deserialize)]\npub struct Settings {\n pub database: DatabaseConfig,\n pub s3: S3Config,\n pub llm: LlmConfig,\n pub tts: TtsConfig,\n pub assets: AssetConfig,\n pub youtube: YouTubeConfig,\n pub channel: ChannelConfig,\n pub content_strategy: ContentStrategyConfig,\n pub script_improvement: ScriptImprovementConfig,\n pub fact_checker: FactCheckerConfig,\n pub notifications: NotificationConfig,\n pub control_api_port: u16,\n}\n\nuse crate::nodes::tts::TTSProvider;\n\n#[derive(Debug, Clone, Deserialize)]\npub struct DatabaseConfig {\n pub url: String,\n}\n\n#[derive(Debug, Clone, Deserialize)]\npub struct S3Config {\n pub endpoint: String,\n pub access_key: String,\n pub secret_key: String,\n pub bucket: String,\n pub region: String,\n}\n\n#[derive(Debug, Clone, Deserialize)]\npub struct LlmConfig {\n pub deepseek_api_key: String,\n pub deepseek_base_url: String,\n pub gemini_api_key: String,\n /// Which provider to use by default: \"deepseek\" or \"gemini\"\n pub default_provider: String,\n}\n\n#[derive(Debug, Clone, Deserialize)]\npub struct TtsConfig {\n /// TTS provider: elevenlabs, qwen3, openai, coqui, piper\n pub provider: TTSProvider,\n\n // ElevenLabs settings\n pub elevenlabs_api_key: Option<String>,\n pub elevenlabs_voice_id: Option<String>,\n pub elevenlabs_model_id: Option<String>,\n\n // Qwen3-TTS settings (self-hosted, OpenAI-compatible)\n pub qwen3_api_url: Option<String>,\n pub qwen3_api_key: Option<String>,\n pub qwen3_voice: Option<String>,\n\n // OpenAI TTS settings\n pub openai_api_key: Option<String>,\n pub openai_voice: Option<String>,\n pub openai_model: Option<String>,\n\n // Local TTS settings (Coqui/Piper)\n pub local_model_path: Option<String>,\n pub local_speaker_id: Option<String>,\n\n // Common settings\n pub stability: f32,\n pub similarity_boost: f32,\n pub speed: f32,\n}\n\n#[derive(Debug, Clone, Deserialize)]\npub struct YouTubeConfig {\n pub client_id: String,\n pub client_secret: String,\n pub refresh_token: String,\n /// Default YouTube account ID to use (optional, for single-account setups)\n pub default_account_id: Option<i32>,\n}\n\n#[derive(Debug, Clone, Deserialize)]\npub struct ChannelConfig {\n pub name: String,\n pub tagline: String,\n}\n\n#[derive(Debug, Clone, Deserialize)]\npub struct ContentStrategyConfig {\n pub target_duration_min: u32,\n pub target_duration_max: u32,\n pub videos_per_week: u32,\n}\n\n#[derive(Debug, Clone, Deserialize)]\npub struct NotificationConfig {\n pub discord_webhook_url: Option<String>,\n pub smtp_host: Option<String>,\n pub smtp_port: Option<u16>,\n pub smtp_user: Option<String>,\n pub smtp_pass: Option<String>,\n pub notification_email: Option<String>,\n}\n\n/// Pexels and AI asset services config\n#[derive(Debug, Clone, Deserialize)]\npub struct AssetConfig {\n pub pexels_api_key: String,\n pub leonardo_api_key: Option<String>,\n pub freesound_api_key: Option<String>,\n pub sd_api_url: Option<String>,\n pub sd_api_key: Option<String>,\n pub min_clips_per_section: u32,\n /// Maximum retry attempts for downloading videos\n pub max_retries: u32,\n /// Minimum file size in KB to consider video valid\n pub min_file_size_kb: u64,\n /// Whether to validate videos with ffprobe (if available)\n pub validate_with_ffprobe: bool,\n /// Whether to fallback to AI images when videos fail\n pub fallback_to_images: bool,\n}\n\n#[derive(Debug, Clone, Deserialize)]\npub struct FactCheckerConfig {\n pub enabled: bool,\n pub groq_api_key: String,\n pub strict_mode: bool,\n pub verify_wisdom_quotes: bool,\n pub verify_historical: bool,\n pub verify_general: bool,\n pub rewrite_trigger_threshold: f64,\n pub max_claims_per_script: u32,\n pub fail_open: bool,\n}\n\n/// Script self-improvement loop configuration\n#[derive(Debug, Clone, Deserialize)]\npub struct ScriptImprovementConfig {\n /// Enable the self-improvement loop\n pub enabled: bool,\n /// Number of initial script candidates to generate in parallel\n pub candidate_count: u32,\n /// Minimum score (1.0-10.0) to accept without refinement\n pub quality_threshold: f32,\n /// Maximum refinement iterations before accepting best available\n pub max_iterations: u32,\n /// Number of iterations without improvement before giving up early\n pub stagnation_threshold: u32,\n /// Total timeout for the entire self-improvement process (seconds)\n pub timeout_seconds: u32,\n}\n\nimpl Settings {\n /// Load settings from environment variables\n #[allow(unsafe_code)]\n pub fn from_env() -> Result<Self, config::ConfigError> {\n #[cfg(not(test))]\n dotenvy::dotenv().ok();\n\n let settings = config::Config::builder()\n // Database\n .set_default(\"database.url\", \"\")?\n .set_override_option(\"database.url\", std::env::var(\"DATABASE_URL\").ok())?\n // S3\n .set_default(\"s3.endpoint\", \"http://localhost:9000\")?\n .set_override_option(\"s3.endpoint\", std::env::var(\"S3_ENDPOINT\").ok())?\n .set_default(\"s3.access_key\", \"minioadmin\")?\n .set_override_option(\"s3.access_key\", std::env::var(\"S3_ACCESS_KEY\").ok())?\n .set_default(\"s3.secret_key\", \"minioadmin\")?\n .set_override_option(\"s3.secret_key\", std::env::var(\"S3_SECRET_KEY\").ok())?\n .set_default(\"s3.bucket\", \"animus-assets\")?\n .set_override_option(\"s3.bucket\", std::env::var(\"S3_BUCKET\").ok())?\n .set_default(\"s3.region\", \"us-east-1\")?\n .set_override_option(\"s3.region\", std::env::var(\"S3_REGION\").ok())?\n // LLM\n .set_default(\"llm.deepseek_api_key\", \"\")?\n .set_override_option(\n \"llm.deepseek_api_key\",\n std::env::var(\"DEEPSEEK_API_KEY\").ok(),\n )?\n .set_default(\"llm.deepseek_base_url\", \"https://api.deepseek.com\")?\n .set_override_option(\n \"llm.deepseek_base_url\",\n std::env::var(\"DEEPSEEK_BASE_URL\").ok(),\n )?\n .set_default(\"llm.gemini_api_key\", \"\")?\n .set_override_option(\"llm.gemini_api_key\", std::env::var(\"GEMINI_API_KEY\").ok())?\n .set_default(\"llm.default_provider\", \"deepseek\")?\n .set_override_option(\n \"llm.default_provider\",\n std::env::var(\"LLM_DEFAULT_PROVIDER\").ok(),\n )?\n // TTS\n .set_default(\"tts.provider\", \"elevenlabs\")?\n .set_override_option(\"tts.provider\", std::env::var(\"TTS_PROVIDER\").ok())?\n // ElevenLabs\n .set_default(\"tts.elevenlabs_api_key\", Option::<String>::None)?\n .set_override_option(\n \"tts.elevenlabs_api_key\",\n std::env::var(\"ELEVENLABS_API_KEY\").ok(),\n )?\n .set_default(\"tts.elevenlabs_voice_id\", Option::<String>::None)?\n .set_override_option(\n \"tts.elevenlabs_voice_id\",\n std::env::var(\"ELEVENLABS_VOICE_ID\").ok(),\n )?\n .set_default(\"tts.elevenlabs_model_id\", Option::<String>::None)?\n .set_override_option(\n \"tts.elevenlabs_model_id\",\n std::env::var(\"ELEVENLABS_MODEL_ID\").ok(),\n )?\n // Qwen3-TTS\n .set_default(\"tts.qwen3_api_url\", Option::<String>::None)?\n .set_override_option(\"tts.qwen3_api_url\", std::env::var(\"QWEN3_API_URL\").ok())?\n .set_default(\"tts.qwen3_api_key\", Option::<String>::None)?\n .set_override_option(\"tts.qwen3_api_key\", std::env::var(\"QWEN3_API_KEY\").ok())?\n .set_default(\"tts.qwen3_voice\", Option::<String>::None)?\n .set_override_option(\"tts.qwen3_voice\", std::env::var(\"QWEN3_VOICE\").ok())?\n // OpenAI TTS\n .set_default(\"tts.openai_api_key\", Option::<String>::None)?\n .set_override_option(\n \"tts.openai_api_key\",\n std::env::var(\"OPENAI_TTS_API_KEY\").ok(),\n )?\n .set_default(\"tts.openai_voice\", Option::<String>::None)?\n .set_override_option(\"tts.openai_voice\", std::env::var(\"OPENAI_TTS_VOICE\").ok())?\n .set_default(\"tts.openai_model\", Option::<String>::None)?\n .set_override_option(\"tts.openai_model\", std::env::var(\"OPENAI_TTS_MODEL\").ok())?\n // Local TTS\n .set_default(\"tts.local_model_path\", Option::<String>::None)?\n .set_override_option(\n \"tts.local_model_path\",\n std::env::var(\"LOCAL_TTS_MODEL_PATH\").ok(),\n )?\n .set_default(\"tts.local_speaker_id\", Option::<String>::None)?\n .set_override_option(\n \"tts.local_speaker_id\",\n std::env::var(\"LOCAL_TTS_SPEAKER_ID\").ok(),\n )?\n // Common TTS settings\n .set_default(\"tts.stability\", 0.5)?\n .set_default(\"tts.similarity_boost\", 0.75)?\n .set_default(\"tts.speed\", 1.0)?\n .set_override_option(\n \"tts.speed\",\n std::env::var(\"TTS_SPEED\")\n .ok()\n .and_then(|v| v.parse::<f64>().ok()),\n )?\n // Assets (Pexels, AI services, sound effects)\n .set_default(\"assets.pexels_api_key\", \"\")?\n .set_override_option(\n \"assets.pexels_api_key\",\n std::env::var(\"PEXELS_API_KEY\").ok(),\n )?\n .set_default(\"assets.leonardo_api_key\", Option::<String>::None)?\n .set_override_option(\n \"assets.leonardo_api_key\",\n std::env::var(\"LEONARDO_API_KEY\")\n .or_else(|_| std::env::var(\"LEONARDOAI_API_KEY\"))\n .ok()\n .filter(|s| !s.is_empty()),\n )?\n .set_default(\"assets.freesound_api_key\", Option::<String>::None)?\n .set_override_option(\n \"assets.freesound_api_key\",\n std::env::var(\"FREESOUND_API_KEY\")\n .ok()\n .filter(|s| !s.is_empty()),\n )?\n .set_default(\"assets.sd_api_url\", Option::<String>::None)?\n .set_override_option(\n \"assets.sd_api_url\",\n std::env::var(\"SD_API_URL\").ok().filter(|s| !s.is_empty()),\n )?\n .set_default(\"assets.sd_api_key\", Option::<String>::None)?\n .set_override_option(\n \"assets.sd_api_key\",\n std::env::var(\"SD_API_KEY\").ok().filter(|s| !s.is_empty()),\n )?\n .set_default(\"assets.min_clips_per_section\", 3)?\n .set_override_option(\n \"assets.min_clips_per_section\",\n std::env::var(\"ASSET_MIN_CLIPS_PER_SECTION\")\n .ok()\n .and_then(|v| v.parse::<i64>().ok()),\n )?\n .set_default(\"assets.max_retries\", 3)?\n .set_override_option(\n \"assets.max_retries\",\n std::env::var(\"ASSET_MAX_RETRIES\")\n .ok()\n .and_then(|v| v.parse::<i64>().ok()),\n )?\n .set_default(\"assets.min_file_size_kb\", 100)?\n .set_override_option(\n \"assets.min_file_size_kb\",\n std::env::var(\"ASSET_MIN_FILE_SIZE_KB\")\n .ok()\n .and_then(|v| v.parse::<i64>().ok()),\n )?\n .set_default(\"assets.validate_with_ffprobe\", true)?\n .set_override_option(\n \"assets.validate_with_ffprobe\",\n std::env::var(\"ASSET_VALIDATE_WITH_FFPROBE\")\n .ok()\n .map(|v| v.to_lowercase() == \"true\" || v == \"1\"),\n )?\n .set_default(\"assets.fallback_to_images\", true)?\n .set_override_option(\n \"assets.fallback_to_images\",\n std::env::var(\"ASSET_FALLBACK_TO_IMAGES\")\n .ok()\n .map(|v| v.to_lowercase() == \"true\" || v == \"1\"),\n )?\n // YouTube\n .set_default(\"youtube.client_id\", \"\")?\n .set_override_option(\"youtube.client_id\", std::env::var(\"YOUTUBE_CLIENT_ID\").ok())?\n .set_default(\"youtube.client_secret\", \"\")?\n .set_override_option(\n \"youtube.client_secret\",\n std::env::var(\"YOUTUBE_CLIENT_SECRET\").ok(),\n )?\n .set_default(\"youtube.refresh_token\", \"\")?\n .set_override_option(\n \"youtube.refresh_token\",\n std::env::var(\"YOUTUBE_REFRESH_TOKEN\").ok(),\n )?\n .set_default(\"youtube.default_account_id\", Option::<i32>::None)?\n .set_override_option(\n \"youtube.default_account_id\",\n std::env::var(\"YOUTUBE_DEFAULT_ACCOUNT_ID\")\n .ok()\n .and_then(|v| v.parse::<i32>().ok()),\n )?\n // Channel\n .set_default(\"channel.name\", \"Excelsior Academy\")?\n .set_override_option(\"channel.name\", std::env::var(\"CHANNEL_NAME\").ok())?\n .set_default(\"channel.tagline\", \"Wisdom for the journey upward\")?\n .set_override_option(\"channel.tagline\", std::env::var(\"CHANNEL_TAGLINE\").ok())?\n // Content Strategy\n .set_default(\"content_strategy.target_duration_min\", 12)?\n .set_override_option(\n \"content_strategy.target_duration_min\",\n std::env::var(\"TARGET_DURATION_MIN\")\n .ok()\n .and_then(|v| v.parse::<i64>().ok()),\n )?\n .set_default(\"content_strategy.target_duration_max\", 20)?\n .set_override_option(\n \"content_strategy.target_duration_max\",\n std::env::var(\"TARGET_DURATION_MAX\")\n .ok()\n .and_then(|v| v.parse::<i64>().ok()),\n )?\n .set_default(\"content_strategy.videos_per_week\", 4)?\n .set_override_option(\n \"content_strategy.videos_per_week\",\n std::env::var(\"VIDEOS_PER_WEEK\")\n .ok()\n .and_then(|v| v.parse::<i64>().ok()),\n )?\n // Script Self-Improvement Loop\n .set_default(\"script_improvement.enabled\", true)?\n .set_override_option(\n \"script_improvement.enabled\",\n std::env::var(\"SCRIPT_IMPROVEMENT_ENABLED\")\n .ok()\n .map(|v| v.to_lowercase() == \"true\" || v == \"1\"),\n )?\n .set_default(\"script_improvement.candidate_count\", 3)?\n .set_override_option(\n \"script_improvement.candidate_count\",\n std::env::var(\"SCRIPT_IMPROVEMENT_CANDIDATES\")\n .ok()\n .and_then(|v| v.parse::<i64>().ok()),\n )?\n .set_default(\"script_improvement.quality_threshold\", 9.0)?\n .set_override_option(\n \"script_improvement.quality_threshold\",\n std::env::var(\"SCRIPT_IMPROVEMENT_THRESHOLD\")\n .ok()\n .and_then(|v| v.parse::<f64>().ok()),\n )?\n .set_default(\"script_improvement.max_iterations\", 10)?\n .set_override_option(\n \"script_improvement.max_iterations\",\n std::env::var(\"SCRIPT_IMPROVEMENT_MAX_ITERATIONS\")\n .ok()\n .and_then(|v| v.parse::<i64>().ok()),\n )?\n .set_default(\"script_improvement.stagnation_threshold\", 3)?\n .set_override_option(\n \"script_improvement.stagnation_threshold\",\n std::env::var(\"SCRIPT_IMPROVEMENT_STAGNATION_THRESHOLD\")\n .ok()\n .and_then(|v| v.parse::<i64>().ok()),\n )?\n .set_default(\"script_improvement.timeout_seconds\", 1800)?\n .set_override_option(\n \"script_improvement.timeout_seconds\",\n std::env::var(\"SCRIPT_IMPROVEMENT_TIMEOUT\")\n .ok()\n .and_then(|v| v.parse::<i64>().ok()),\n )?\n // Fact Checker\n .set_default(\"fact_checker.enabled\", true)?\n .set_override_option(\n \"fact_checker.enabled\",\n std::env::var(\"FACT_CHECKER_ENABLED\")\n .ok()\n .map(|v| v.to_lowercase() == \"true\" || v == \"1\"),\n )?\n .set_default(\"fact_checker.groq_api_key\", \"\")?\n .set_override_option(\"fact_checker.groq_api_key\", std::env::var(\"GROQ_API_KEY\").ok())?\n .set_default(\"fact_checker.strict_mode\", false)?\n .set_override_option(\n \"fact_checker.strict_mode\",\n std::env::var(\"FACT_CHECKER_STRICT_MODE\")\n .ok()\n .map(|v| v.to_lowercase() == \"true\" || v == \"1\"),\n )?\n .set_default(\"fact_checker.verify_wisdom_quotes\", true)?\n .set_default(\"fact_checker.verify_historical\", true)?\n .set_default(\"fact_checker.verify_general\", true)?\n .set_default(\"fact_checker.rewrite_trigger_threshold\", 0.2)?\n .set_default(\"fact_checker.max_claims_per_script\", 20)?\n .set_default(\"fact_checker.fail_open\", true)?\n // Notifications\n .set_default(\"notifications.discord_webhook_url\", Option::<String>::None)?\n .set_override_option(\n \"notifications.discord_webhook_url\",\n std::env::var(\"DISCORD_WEBHOOK_URL\").ok(),\n )?\n .set_default(\"notifications.smtp_host\", Option::<String>::None)?\n .set_override_option(\"notifications.smtp_host\", std::env::var(\"SMTP_HOST\").ok())?\n .set_default(\"notifications.smtp_port\", Option::<u16>::None)?\n .set_override_option(\n \"notifications.smtp_port\",\n std::env::var(\"SMTP_PORT\")\n .ok()\n .and_then(|v| v.parse::<i64>().ok()),\n )?\n .set_default(\"notifications.smtp_user\", Option::<String>::None)?\n .set_override_option(\"notifications.smtp_user\", std::env::var(\"SMTP_USER\").ok())?\n .set_default(\"notifications.smtp_pass\", Option::<String>::None)?\n .set_override_option(\"notifications.smtp_pass\", std::env::var(\"SMTP_PASS\").ok())?\n .set_default(\"notifications.notification_email\", Option::<String>::None)?\n .set_override_option(\n \"notifications.notification_email\",\n std::env::var(\"NOTIFICATION_EMAIL\").ok(),\n )?\n // Control API\n .set_default(\"control_api_port\", 8080)?\n .set_override_option(\n \"control_api_port\",\n std::env::var(\"CONTROL_API_PORT\")\n .ok()\n .and_then(|v| v.parse::<i64>().ok()),\n )?\n .build()?;\n\n settings.try_deserialize()\n }\n}\n\n#[cfg(test)]\nmod tests {\n #![allow(unsafe_code)]\n use super::*;\n\n #[test]\n fn test_default_settings() {\n // This will use defaults since no .env is loaded in tests\n std::env::set_var(\"DATABASE_URL\", \"postgres://test:test@localhost/test\");\n let settings = Settings::from_env();\n assert!(settings.is_ok());\n }\n\n #[test]\n fn test_asset_config_loading() {\n std::env::set_var(\"DATABASE_URL\", \"postgres://test:test@localhost/test\");\n std::env::set_var(\"LEONARDO_API_KEY\", \"test_leonardo_key\");\n std::env::set_var(\"FREESOUND_API_KEY\", \"test_freesound_key\");\n std::env::set_var(\"ASSET_MIN_CLIPS_PER_SECTION\", \"5\");\n\n let settings = Settings::from_env().unwrap();\n assert_eq!(\n settings.assets.leonardo_api_key,\n Some(\"test_leonardo_key\".to_string())\n );\n assert_eq!(\n settings.assets.freesound_api_key,\n Some(\"test_freesound_key\".to_string())\n );\n assert_eq!(settings.assets.min_clips_per_section, 5);\n\n // Test backward compatibility with LEONARDOAI_API_KEY\n std::env::remove_var(\"LEONARDO_API_KEY\");\n std::env::set_var(\"LEONARDOAI_API_KEY\", \"test_leonardoai_key\");\n let settings2 = Settings::from_env().unwrap();\n assert_eq!(\n settings2.assets.leonardo_api_key,\n Some(\"test_leonardoai_key\".to_string())\n );\n\n // Clean up\n std::env::remove_var(\"LEONARDOAI_API_KEY\");\n std::env::remove_var(\"FREESOUND_API_KEY\");\n std::env::remove_var(\"ASSET_MIN_CLIPS_PER_SECTION\");\n }\n}\n"}, "c4e9c374685cae60acfb056de11caa6b4b4b3aad": {"t": 1785757448.9019167, "n": 19339, "h": "5ee216d63443f352", "c": "//! Application settings loaded from environment variables\n\nuse serde::Deserialize;\n\n/// Root configuration structure\n#[derive(Debug, Clone, Deserialize)]\npub struct Settings {\n pub database: DatabaseConfig,\n pub s3: S3Config,\n pub llm: LlmConfig,\n pub tts: TtsConfig,\n pub assets: AssetConfig,\n pub youtube: YouTubeConfig,\n pub channel: ChannelConfig,\n pub content_strategy: ContentStrategyConfig,\n pub script_improvement: ScriptImprovementConfig,\n pub notifications: NotificationConfig,\n pub control_api_port: u16,\n}\n\nuse crate::nodes::tts::TTSProvider;\n\n#[derive(Debug, Clone, Deserialize)]\npub struct DatabaseConfig {\n pub url: String,\n}\n\n#[derive(Debug, Clone, Deserialize)]\npub struct S3Config {\n pub endpoint: String,\n pub access_key: String,\n pub secret_key: String,\n pub bucket: String,\n pub region: String,\n}\n\n#[derive(Debug, Clone, Deserialize)]\npub struct LlmConfig {\n pub deepseek_api_key: String,\n pub deepseek_base_url: String,\n pub gemini_api_key: String,\n /// Which provider to use by default: \"deepseek\" or \"gemini\"\n pub default_provider: String,\n}\n\n#[derive(Debug, Clone, Deserialize)]\npub struct TtsConfig {\n /// TTS provider: elevenlabs, qwen3, openai, coqui, piper\n pub provider: TTSProvider,\n\n // ElevenLabs settings\n pub elevenlabs_api_key: Option<String>,\n pub elevenlabs_voice_id: Option<String>,\n pub elevenlabs_model_id: Option<String>,\n\n // Qwen3-TTS settings (self-hosted, OpenAI-compatible)\n pub qwen3_api_url: Option<String>,\n pub qwen3_api_key: Option<String>,\n pub qwen3_voice: Option<String>,\n\n // OpenAI TTS settings\n pub openai_api_key: Option<String>,\n pub openai_voice: Option<String>,\n pub openai_model: Option<String>,\n\n // Local TTS settings (Coqui/Piper)\n pub local_model_path: Option<String>,\n pub local_speaker_id: Option<String>,\n\n // Common settings\n pub stability: f32,\n pub similarity_boost: f32,\n pub speed: f32,\n}\n\n#[derive(Debug, Clone, Deserialize)]\npub struct YouTubeConfig {\n pub client_id: String,\n pub client_secret: String,\n pub refresh_token: String,\n /// Default YouTube account ID to use (optional, for single-account setups)\n pub default_account_id: Option<i32>,\n}\n\n#[derive(Debug, Clone, Deserialize)]\npub struct ChannelConfig {\n pub name: String,\n pub tagline: String,\n}\n\n#[derive(Debug, Clone, Deserialize)]\npub struct ContentStrategyConfig {\n pub target_duration_min: u32,\n pub target_duration_max: u32,\n pub videos_per_week: u32,\n}\n\n#[derive(Debug, Clone, Deserialize)]\npub struct NotificationConfig {\n pub discord_webhook_url: Option<String>,\n pub smtp_host: Option<String>,\n pub smtp_port: Option<u16>,\n pub smtp_user: Option<String>,\n pub smtp_pass: Option<String>,\n pub notification_email: Option<String>,\n}\n\n/// Pexels and AI asset services config\n#[derive(Debug, Clone, Deserialize)]\npub struct AssetConfig {\n pub pexels_api_key: String,\n pub leonardo_api_key: Option<String>,\n pub freesound_api_key: Option<String>,\n pub sd_api_url: Option<String>,\n pub sd_api_key: Option<String>,\n pub min_clips_per_section: u32,\n /// Maximum retry attempts for downloading videos\n pub max_retries: u32,\n /// Minimum file size in KB to consider video valid\n pub min_file_size_kb: u64,\n /// Whether to validate videos with ffprobe (if available)\n pub validate_with_ffprobe: bool,\n /// Whether to fallback to AI images when videos fail\n pub fallback_to_images: bool,\n}\n\n/// Script self-improvement loop configuration\n#[derive(Debug, Clone, Deserialize)]\npub struct ScriptImprovementConfig {\n /// Enable the self-improvement loop\n pub enabled: bool,\n /// Number of initial script candidates to generate in parallel\n pub candidate_count: u32,\n /// Minimum score (1.0-10.0) to accept without refinement\n pub quality_threshold: f32,\n /// Maximum refinement iterations before accepting best available\n pub max_iterations: u32,\n /// Number of iterations without improvement before giving up early\n pub stagnation_threshold: u32,\n /// Total timeout for the entire self-improvement process (seconds)\n pub timeout_seconds: u32,\n}\n\nimpl Settings {\n /// Load settings from environment variables\n #[allow(unsafe_code)]\n pub fn from_env() -> Result<Self, config::ConfigError> {\n #[cfg(not(test))]\n dotenvy::dotenv().ok();\n\n let settings = config::Config::builder()\n // Database\n .set_default(\"database.url\", \"\")?\n .set_override_option(\"database.url\", std::env::var(\"DATABASE_URL\").ok())?\n // S3\n .set_default(\"s3.endpoint\", \"http://localhost:9000\")?\n .set_override_option(\"s3.endpoint\", std::env::var(\"S3_ENDPOINT\").ok())?\n .set_default(\"s3.access_key\", \"minioadmin\")?\n .set_override_option(\"s3.access_key\", std::env::var(\"S3_ACCESS_KEY\").ok())?\n .set_default(\"s3.secret_key\", \"minioadmin\")?\n .set_override_option(\"s3.secret_key\", std::env::var(\"S3_SECRET_KEY\").ok())?\n .set_default(\"s3.bucket\", \"animus-assets\")?\n .set_override_option(\"s3.bucket\", std::env::var(\"S3_BUCKET\").ok())?\n .set_default(\"s3.region\", \"us-east-1\")?\n .set_override_option(\"s3.region\", std::env::var(\"S3_REGION\").ok())?\n // LLM\n .set_default(\"llm.deepseek_api_key\", \"\")?\n .set_override_option(\n \"llm.deepseek_api_key\",\n std::env::var(\"DEEPSEEK_API_KEY\").ok(),\n )?\n .set_default(\"llm.deepseek_base_url\", \"https://api.deepseek.com\")?\n .set_override_option(\n \"llm.deepseek_base_url\",\n std::env::var(\"DEEPSEEK_BASE_URL\").ok(),\n )?\n .set_default(\"llm.gemini_api_key\", \"\")?\n .set_override_option(\"llm.gemini_api_key\", std::env::var(\"GEMINI_API_KEY\").ok())?\n .set_default(\"llm.default_provider\", \"deepseek\")?\n .set_override_option(\n \"llm.default_provider\",\n std::env::var(\"LLM_DEFAULT_PROVIDER\").ok(),\n )?\n // TTS\n .set_default(\"tts.provider\", \"elevenlabs\")?\n .set_override_option(\"tts.provider\", std::env::var(\"TTS_PROVIDER\").ok())?\n // ElevenLabs\n .set_default(\"tts.elevenlabs_api_key\", Option::<String>::None)?\n .set_override_option(\n \"tts.elevenlabs_api_key\",\n std::env::var(\"ELEVENLABS_API_KEY\").ok(),\n )?\n .set_default(\"tts.elevenlabs_voice_id\", Option::<String>::None)?\n .set_override_option(\n \"tts.elevenlabs_voice_id\",\n std::env::var(\"ELEVENLABS_VOICE_ID\").ok(),\n )?\n .set_default(\"tts.elevenlabs_model_id\", Option::<String>::None)?\n .set_override_option(\n \"tts.elevenlabs_model_id\",\n std::env::var(\"ELEVENLABS_MODEL_ID\").ok(),\n )?\n // Qwen3-TTS\n .set_default(\"tts.qwen3_api_url\", Option::<String>::None)?\n .set_override_option(\"tts.qwen3_api_url\", std::env::var(\"QWEN3_API_URL\").ok())?\n .set_default(\"tts.qwen3_api_key\", Option::<String>::None)?\n .set_override_option(\"tts.qwen3_api_key\", std::env::var(\"QWEN3_API_KEY\").ok())?\n .set_default(\"tts.qwen3_voice\", Option::<String>::None)?\n .set_override_option(\"tts.qwen3_voice\", std::env::var(\"QWEN3_VOICE\").ok())?\n // OpenAI TTS\n .set_default(\"tts.openai_api_key\", Option::<String>::None)?\n .set_override_option(\n \"tts.openai_api_key\",\n std::env::var(\"OPENAI_TTS_API_KEY\").ok(),\n )?\n .set_default(\"tts.openai_voice\", Option::<String>::None)?\n .set_override_option(\"tts.openai_voice\", std::env::var(\"OPENAI_TTS_VOICE\").ok())?\n .set_default(\"tts.openai_model\", Option::<String>::None)?\n .set_override_option(\"tts.openai_model\", std::env::var(\"OPENAI_TTS_MODEL\").ok())?\n // Local TTS\n .set_default(\"tts.local_model_path\", Option::<String>::None)?\n .set_override_option(\n \"tts.local_model_path\",\n std::env::var(\"LOCAL_TTS_MODEL_PATH\").ok(),\n )?\n .set_default(\"tts.local_speaker_id\", Option::<String>::None)?\n .set_override_option(\n \"tts.local_speaker_id\",\n std::env::var(\"LOCAL_TTS_SPEAKER_ID\").ok(),\n )?\n // Common TTS settings\n .set_default(\"tts.stability\", 0.5)?\n .set_default(\"tts.similarity_boost\", 0.75)?\n .set_default(\"tts.speed\", 1.0)?\n .set_override_option(\n \"tts.speed\",\n std::env::var(\"TTS_SPEED\")\n .ok()\n .and_then(|v| v.parse::<f64>().ok()),\n )?\n // Assets (Pexels, AI services, sound effects)\n .set_default(\"assets.pexels_api_key\", \"\")?\n .set_override_option(\n \"assets.pexels_api_key\",\n std::env::var(\"PEXELS_API_KEY\").ok(),\n )?\n .set_default(\"assets.leonardo_api_key\", Option::<String>::None)?\n .set_override_option(\n \"assets.leonardo_api_key\",\n std::env::var(\"LEONARDO_API_KEY\")\n .or_else(|_| std::env::var(\"LEONARDOAI_API_KEY\"))\n .ok()\n .filter(|s| !s.is_empty()),\n )?\n .set_default(\"assets.freesound_api_key\", Option::<String>::None)?\n .set_override_option(\n \"assets.freesound_api_key\",\n std::env::var(\"FREESOUND_API_KEY\")\n .ok()\n .filter(|s| !s.is_empty()),\n )?\n .set_default(\"assets.sd_api_url\", Option::<String>::None)?\n .set_override_option(\n \"assets.sd_api_url\",\n std::env::var(\"SD_API_URL\").ok().filter(|s| !s.is_empty()),\n )?\n .set_default(\"assets.sd_api_key\", Option::<String>::None)?\n .set_override_option(\n \"assets.sd_api_key\",\n std::env::var(\"SD_API_KEY\").ok().filter(|s| !s.is_empty()),\n )?\n .set_default(\"assets.min_clips_per_section\", 3)?\n .set_override_option(\n \"assets.min_clips_per_section\",\n std::env::var(\"ASSET_MIN_CLIPS_PER_SECTION\")\n .ok()\n .and_then(|v| v.parse::<i64>().ok()),\n )?\n .set_default(\"assets.max_retries\", 3)?\n .set_override_option(\n \"assets.max_retries\",\n std::env::var(\"ASSET_MAX_RETRIES\")\n .ok()\n .and_then(|v| v.parse::<i64>().ok()),\n )?\n .set_default(\"assets.min_file_size_kb\", 100)?\n .set_override_option(\n \"assets.min_file_size_kb\",\n std::env::var(\"ASSET_MIN_FILE_SIZE_KB\")\n .ok()\n .and_then(|v| v.parse::<i64>().ok()),\n )?\n .set_default(\"assets.validate_with_ffprobe\", true)?\n .set_override_option(\n \"assets.validate_with_ffprobe\",\n std::env::var(\"ASSET_VALIDATE_WITH_FFPROBE\")\n .ok()\n .map(|v| v.to_lowercase() == \"true\" || v == \"1\"),\n )?\n .set_default(\"assets.fallback_to_images\", true)?\n .set_override_option(\n \"assets.fallback_to_images\",\n std::env::var(\"ASSET_FALLBACK_TO_IMAGES\")\n .ok()\n .map(|v| v.to_lowercase() == \"true\" || v == \"1\"),\n )?\n // YouTube\n .set_default(\"youtube.client_id\", \"\")?\n .set_override_option(\"youtube.client_id\", std::env::var(\"YOUTUBE_CLIENT_ID\").ok())?\n .set_default(\"youtube.client_secret\", \"\")?\n .set_override_option(\n \"youtube.client_secret\",\n std::env::var(\"YOUTUBE_CLIENT_SECRET\").ok(),\n )?\n .set_default(\"youtube.refresh_token\", \"\")?\n .set_override_option(\n \"youtube.refresh_token\",\n std::env::var(\"YOUTUBE_REFRESH_TOKEN\").ok(),\n )?\n .set_default(\"youtube.default_account_id\", Option::<i32>::None)?\n .set_override_option(\n \"youtube.default_account_id\",\n std::env::var(\"YOUTUBE_DEFAULT_ACCOUNT_ID\")\n .ok()\n .and_then(|v| v.parse::<i32>().ok()),\n )?\n // Channel\n .set_default(\"channel.name\", \"Excelsior Academy\")?\n .set_override_option(\"channel.name\", std::env::var(\"CHANNEL_NAME\").ok())?\n .set_default(\"channel.tagline\", \"Wisdom for the journey upward\")?\n .set_override_option(\"channel.tagline\", std::env::var(\"CHANNEL_TAGLINE\").ok())?\n // Content Strategy\n .set_default(\"content_strategy.target_duration_min\", 12)?\n .set_override_option(\n \"content_strategy.target_duration_min\",\n std::env::var(\"TARGET_DURATION_MIN\")\n .ok()\n .and_then(|v| v.parse::<i64>().ok()),\n )?\n .set_default(\"content_strategy.target_duration_max\", 20)?\n .set_override_option(\n \"content_strategy.target_duration_max\",\n std::env::var(\"TARGET_DURATION_MAX\")\n .ok()\n .and_then(|v| v.parse::<i64>().ok()),\n )?\n .set_default(\"content_strategy.videos_per_week\", 4)?\n .set_override_option(\n \"content_strategy.videos_per_week\",\n std::env::var(\"VIDEOS_PER_WEEK\")\n .ok()\n .and_then(|v| v.parse::<i64>().ok()),\n )?\n // Script Self-Improvement Loop\n .set_default(\"script_improvement.enabled\", true)?\n .set_override_option(\n \"script_improvement.enabled\",\n std::env::var(\"SCRIPT_IMPROVEMENT_ENABLED\")\n .ok()\n .map(|v| v.to_lowercase() == \"true\" || v == \"1\"),\n )?\n .set_default(\"script_improvement.candidate_count\", 3)?\n .set_override_option(\n \"script_improvement.candidate_count\",\n std::env::var(\"SCRIPT_IMPROVEMENT_CANDIDATES\")\n .ok()\n .and_then(|v| v.parse::<i64>().ok()),\n )?\n .set_default(\"script_improvement.quality_threshold\", 9.0)?\n .set_override_option(\n \"script_improvement.quality_threshold\",\n std::env::var(\"SCRIPT_IMPROVEMENT_THRESHOLD\")\n .ok()\n .and_then(|v| v.parse::<f64>().ok()),\n )?\n .set_default(\"script_improvement.max_iterations\", 10)?\n .set_override_option(\n \"script_improvement.max_iterations\",\n std::env::var(\"SCRIPT_IMPROVEMENT_MAX_ITERATIONS\")\n .ok()\n .and_then(|v| v.parse::<i64>().ok()),\n )?\n .set_default(\"script_improvement.stagnation_threshold\", 3)?\n .set_override_option(\n \"script_improvement.stagnation_threshold\",\n std::env::var(\"SCRIPT_IMPROVEMENT_STAGNATION_THRESHOLD\")\n .ok()\n .and_then(|v| v.parse::<i64>().ok()),\n )?\n .set_default(\"script_improvement.timeout_seconds\", 1800)?\n .set_override_option(\n \"script_improvement.timeout_seconds\",\n std::env::var(\"SCRIPT_IMPROVEMENT_TIMEOUT\")\n .ok()\n .and_then(|v| v.parse::<i64>().ok()),\n )?\n // Notifications\n .set_default(\"notifications.discord_webhook_url\", Option::<String>::None)?\n .set_override_option(\n \"notifications.discord_webhook_url\",\n std::env::var(\"DISCORD_WEBHOOK_URL\").ok(),\n )?\n .set_default(\"notifications.smtp_host\", Option::<String>::None)?\n .set_override_option(\"notifications.smtp_host\", std::env::var(\"SMTP_HOST\").ok())?\n .set_default(\"notifications.smtp_port\", Option::<u16>::None)?\n .set_override_option(\n \"notifications.smtp_port\",\n std::env::var(\"SMTP_PORT\")\n .ok()\n .and_then(|v| v.parse::<i64>().ok()),\n )?\n .set_default(\"notifications.smtp_user\", Option::<String>::None)?\n .set_override_option(\"notifications.smtp_user\", std::env::var(\"SMTP_USER\").ok())?\n .set_default(\"notifications.smtp_pass\", Option::<String>::None)?\n .set_override_option(\"notifications.smtp_pass\", std::env::var(\"SMTP_PASS\").ok())?\n .set_default(\"notifications.notification_email\", Option::<String>::None)?\n .set_override_option(\n \"notifications.notification_email\",\n std::env::var(\"NOTIFICATION_EMAIL\").ok(),\n )?\n // Control API\n .set_default(\"control_api_port\", 8080)?\n .set_override_option(\n \"control_api_port\",\n std::env::var(\"CONTROL_API_PORT\")\n .ok()\n .and_then(|v| v.parse::<i64>().ok()),\n )?\n .build()?;\n\n settings.try_deserialize()\n }\n}\n\n#[cfg(test)]\nmod tests {\n #![allow(unsafe_code)]\n use super::*;\n\n #[test]\n fn test_default_settings() {\n // This will use defaults since no .env is loaded in tests\n std::env::set_var(\"DATABASE_URL\", \"postgres://test:test@localhost/test\");\n let settings = Settings::from_env();\n assert!(settings.is_ok());\n }\n\n #[test]\n fn test_asset_config_loading() {\n std::env::set_var(\"DATABASE_URL\", \"postgres://test:test@localhost/test\");\n std::env::set_var(\"LEONARDO_API_KEY\", \"test_leonardo_key\");\n std::env::set_var(\"FREESOUND_API_KEY\", \"test_freesound_key\");\n std::env::set_var(\"ASSET_MIN_CLIPS_PER_SECTION\", \"5\");\n\n let settings = Settings::from_env().unwrap();\n assert_eq!(\n settings.assets.leonardo_api_key,\n Some(\"test_leonardo_key\".to_string())\n );\n assert_eq!(\n settings.assets.freesound_api_key,\n Some(\"test_freesound_key\".to_string())\n );\n assert_eq!(settings.assets.min_clips_per_section, 5);\n\n // Test backward compatibility with LEONARDOAI_API_KEY\n std::env::remove_var(\"LEONARDO_API_KEY\");\n std::env::set_var(\"LEONARDOAI_API_KEY\", \"test_leonardoai_key\");\n let settings2 = Settings::from_env().unwrap();\n assert_eq!(\n settings2.assets.leonardo_api_key,\n Some(\"test_leonardoai_key\".to_string())\n );\n\n // Clean up\n std::env::remove_var(\"LEONARDOAI_API_KEY\");\n std::env::remove_var(\"FREESOUND_API_KEY\");\n std::env::remove_var(\"ASSET_MIN_CLIPS_PER_SECTION\");\n }\n}\n"}, "f844823b85e3fd84226b0acea2770e090bd44d78": {"t": 1785757450.2902706, "n": 18520, "h": "71ce128935248a06", "c": "//! Application settings loaded from environment variables\n\nuse serde::Deserialize;\n\n/// Root configuration structure\n#[derive(Debug, Clone, Deserialize)]\npub struct Settings {\n pub database: DatabaseConfig,\n pub s3: S3Config,\n pub llm: LlmConfig,\n pub tts: TtsConfig,\n pub assets: AssetConfig,\n pub youtube: YouTubeConfig,\n pub channel: ChannelConfig,\n pub content_strategy: ContentStrategyConfig,\n pub script_improvement: ScriptImprovementConfig,\n pub notifications: NotificationConfig,\n pub control_api_port: u16,\n}\n\nuse crate::nodes::tts::TTSProvider;\n\n#[derive(Debug, Clone, Deserialize)]\npub struct DatabaseConfig {\n pub url: String,\n}\n\n#[derive(Debug, Clone, Deserialize)]\npub struct S3Config {\n pub endpoint: String,\n pub access_key: String,\n pub secret_key: String,\n pub bucket: String,\n pub region: String,\n}\n\n#[derive(Debug, Clone, Deserialize)]\npub struct LlmConfig {\n pub deepseek_api_key: String,\n pub deepseek_base_url: String,\n pub gemini_api_key: String,\n /// Which provider to use by default: \"deepseek\" or \"gemini\"\n pub default_provider: String,\n}\n\n#[derive(Debug, Clone, Deserialize)]\npub struct TtsConfig {\n /// TTS provider: elevenlabs, qwen3, openai, coqui, piper\n pub provider: TTSProvider,\n\n // ElevenLabs settings\n pub elevenlabs_api_key: Option<String>,\n pub elevenlabs_voice_id: Option<String>,\n pub elevenlabs_model_id: Option<String>,\n\n // Qwen3-TTS settings (self-hosted, OpenAI-compatible)\n pub qwen3_api_url: Option<String>,\n pub qwen3_api_key: Option<String>,\n pub qwen3_voice: Option<String>,\n\n // OpenAI TTS settings\n pub openai_api_key: Option<String>,\n pub openai_voice: Option<String>,\n pub openai_model: Option<String>,\n\n // Local TTS settings (Coqui/Piper)\n pub local_model_path: Option<String>,\n pub local_speaker_id: Option<String>,\n\n // Common settings\n pub stability: f32,\n pub similarity_boost: f32,\n pub speed: f32,\n}\n\n#[derive(Debug, Clone, Deserialize)]\npub struct YouTubeConfig {\n pub client_id: String,\n pub client_secret: String,\n pub refresh_token: String,\n}\n\n#[derive(Debug, Clone, Deserialize)]\npub struct ChannelConfig {\n pub name: String,\n pub tagline: String,\n}\n\n#[derive(Debug, Clone, Deserialize)]\npub struct ContentStrategyConfig {\n pub target_duration_min: u32,\n pub target_duration_max: u32,\n pub videos_per_week: u32,\n}\n\n#[derive(Debug, Clone, Deserialize)]\npub struct NotificationConfig {\n pub discord_webhook_url: Option<String>,\n pub smtp_host: Option<String>,\n pub smtp_port: Option<u16>,\n pub smtp_user: Option<String>,\n pub smtp_pass: Option<String>,\n pub notification_email: Option<String>,\n}\n\n/// Pexels and AI asset services config\n#[derive(Debug, Clone, Deserialize)]\npub struct AssetConfig {\n pub pexels_api_key: String,\n pub leonardo_api_key: Option<String>,\n pub freesound_api_key: Option<String>,\n pub sd_api_url: Option<String>,\n pub sd_api_key: Option<String>,\n pub min_clips_per_section: u32,\n /// Maximum retry attempts for downloading videos\n pub max_retries: u32,\n /// Minimum file size in KB to consider video valid\n pub min_file_size_kb: u64,\n /// Whether to validate videos with ffprobe (if available)\n pub validate_with_ffprobe: bool,\n /// Whether to fallback to AI images when videos fail\n pub fallback_to_images: bool,\n}\n\n/// Script self-improvement loop configuration\n#[derive(Debug, Clone, Deserialize)]\npub struct ScriptImprovementConfig {\n /// Enable the self-improvement loop\n pub enabled: bool,\n /// Number of initial script candidates to generate in parallel\n pub candidate_count: u32,\n /// Minimum score (1.0-10.0) to accept without refinement\n pub quality_threshold: f32,\n /// Maximum refinement iterations before accepting best available\n pub max_iterations: u32,\n /// Number of iterations without improvement before giving up early\n pub stagnation_threshold: u32,\n /// Total timeout for the entire self-improvement process (seconds)\n pub timeout_seconds: u32,\n}\n\nimpl Settings {\n /// Load settings from environment variables\n pub fn from_env() -> Result<Self, config::ConfigError> {\n dotenvy::dotenv().ok();\n\n let settings = config::Config::builder()\n // Database\n .set_default(\"database.url\", \"\")?\n .set_override_option(\n \"database.url\",\n std::env::var(\"DATABASE_URL\").ok(),\n )?\n // S3\n .set_default(\"s3.endpoint\", \"http://localhost:9000\")?\n .set_override_option(\"s3.endpoint\", std::env::var(\"S3_ENDPOINT\").ok())?\n .set_default(\"s3.access_key\", \"minioadmin\")?\n .set_override_option(\"s3.access_key\", std::env::var(\"S3_ACCESS_KEY\").ok())?\n .set_default(\"s3.secret_key\", \"minioadmin\")?\n .set_override_option(\"s3.secret_key\", std::env::var(\"S3_SECRET_KEY\").ok())?\n .set_default(\"s3.bucket\", \"animus-assets\")?\n .set_override_option(\"s3.bucket\", std::env::var(\"S3_BUCKET\").ok())?\n .set_default(\"s3.region\", \"us-east-1\")?\n .set_override_option(\"s3.region\", std::env::var(\"S3_REGION\").ok())?\n // LLM\n .set_default(\"llm.deepseek_api_key\", \"\")?\n .set_override_option(\n \"llm.deepseek_api_key\",\n std::env::var(\"DEEPSEEK_API_KEY\").ok(),\n )?\n .set_default(\"llm.deepseek_base_url\", \"https://api.deepseek.com\")?\n .set_override_option(\n \"llm.deepseek_base_url\",\n std::env::var(\"DEEPSEEK_BASE_URL\").ok(),\n )?\n .set_default(\"llm.gemini_api_key\", \"\")?\n .set_override_option(\"llm.gemini_api_key\", std::env::var(\"GEMINI_API_KEY\").ok())?\n .set_default(\"llm.default_provider\", \"deepseek\")?\n .set_override_option(\n \"llm.default_provider\",\n std::env::var(\"LLM_DEFAULT_PROVIDER\").ok(),\n )?\n // TTS\n .set_default(\"tts.provider\", \"elevenlabs\")?\n .set_override_option(\"tts.provider\", std::env::var(\"TTS_PROVIDER\").ok())?\n // ElevenLabs\n .set_default(\"tts.elevenlabs_api_key\", Option::<String>::None)?\n .set_override_option(\n \"tts.elevenlabs_api_key\",\n std::env::var(\"ELEVENLABS_API_KEY\").ok(),\n )?\n .set_default(\"tts.elevenlabs_voice_id\", Option::<String>::None)?\n .set_override_option(\n \"tts.elevenlabs_voice_id\",\n std::env::var(\"ELEVENLABS_VOICE_ID\").ok(),\n )?\n .set_default(\"tts.elevenlabs_model_id\", Option::<String>::None)?\n .set_override_option(\n \"tts.elevenlabs_model_id\",\n std::env::var(\"ELEVENLABS_MODEL_ID\").ok(),\n )?\n // Qwen3-TTS\n .set_default(\"tts.qwen3_api_url\", Option::<String>::None)?\n .set_override_option(\"tts.qwen3_api_url\", std::env::var(\"QWEN3_API_URL\").ok())?\n .set_default(\"tts.qwen3_api_key\", Option::<String>::None)?\n .set_override_option(\"tts.qwen3_api_key\", std::env::var(\"QWEN3_API_KEY\").ok())?\n .set_default(\"tts.qwen3_voice\", Option::<String>::None)?\n .set_override_option(\"tts.qwen3_voice\", std::env::var(\"QWEN3_VOICE\").ok())?\n // OpenAI TTS\n .set_default(\"tts.openai_api_key\", Option::<String>::None)?\n .set_override_option(\"tts.openai_api_key\", std::env::var(\"OPENAI_TTS_API_KEY\").ok())?\n .set_default(\"tts.openai_voice\", Option::<String>::None)?\n .set_override_option(\"tts.openai_voice\", std::env::var(\"OPENAI_TTS_VOICE\").ok())?\n .set_default(\"tts.openai_model\", Option::<String>::None)?\n .set_override_option(\"tts.openai_model\", std::env::var(\"OPENAI_TTS_MODEL\").ok())?\n // Local TTS\n .set_default(\"tts.local_model_path\", Option::<String>::None)?\n .set_override_option(\"tts.local_model_path\", std::env::var(\"LOCAL_TTS_MODEL_PATH\").ok())?\n .set_default(\"tts.local_speaker_id\", Option::<String>::None)?\n .set_override_option(\"tts.local_speaker_id\", std::env::var(\"LOCAL_TTS_SPEAKER_ID\").ok())?\n // Common TTS settings\n .set_default(\"tts.stability\", 0.5)?\n .set_default(\"tts.similarity_boost\", 0.75)?\n .set_default(\"tts.speed\", 1.0)?\n .set_override_option(\n \"tts.speed\",\n std::env::var(\"TTS_SPEED\").ok().and_then(|v| v.parse::<f64>().ok()),\n )?\n // Assets (Pexels, AI services, sound effects)\n .set_default(\"assets.pexels_api_key\", \"\")?\n .set_override_option(\n \"assets.pexels_api_key\",\n std::env::var(\"PEXELS_API_KEY\").ok(),\n )?\n .set_default(\"assets.leonardo_api_key\", Option::<String>::None)?\n .set_override_option(\n \"assets.leonardo_api_key\",\n std::env::var(\"LEONARDO_API_KEY\")\n .or_else(|_| std::env::var(\"LEONARDOAI_API_KEY\"))\n .ok()\n .filter(|s| !s.is_empty()),\n )?\n .set_default(\"assets.freesound_api_key\", Option::<String>::None)?\n .set_override_option(\n \"assets.freesound_api_key\",\n std::env::var(\"FREESOUND_API_KEY\").ok().filter(|s| !s.is_empty()),\n )?\n .set_default(\"assets.sd_api_url\", Option::<String>::None)?\n .set_override_option(\"assets.sd_api_url\", std::env::var(\"SD_API_URL\").ok().filter(|s| !s.is_empty()))?\n .set_default(\"assets.sd_api_key\", Option::<String>::None)?\n .set_override_option(\"assets.sd_api_key\", std::env::var(\"SD_API_KEY\").ok().filter(|s| !s.is_empty()))?\n .set_default(\"assets.min_clips_per_section\", 3)?\n .set_override_option(\n \"assets.min_clips_per_section\",\n std::env::var(\"ASSET_MIN_CLIPS_PER_SECTION\")\n .ok()\n .and_then(|v| v.parse::<i64>().ok()),\n )?\n .set_default(\"assets.max_retries\", 3)?\n .set_override_option(\n \"assets.max_retries\",\n std::env::var(\"ASSET_MAX_RETRIES\")\n .ok()\n .and_then(|v| v.parse::<i64>().ok()),\n )?\n .set_default(\"assets.min_file_size_kb\", 100)?\n .set_override_option(\n \"assets.min_file_size_kb\",\n std::env::var(\"ASSET_MIN_FILE_SIZE_KB\")\n .ok()\n .and_then(|v| v.parse::<i64>().ok()),\n )?\n .set_default(\"assets.validate_with_ffprobe\", true)?\n .set_override_option(\n \"assets.validate_with_ffprobe\",\n std::env::var(\"ASSET_VALIDATE_WITH_FFPROBE\")\n .ok()\n .map(|v| v.to_lowercase() == \"true\" || v == \"1\"),\n )?\n .set_default(\"assets.fallback_to_images\", true)?\n .set_override_option(\n \"assets.fallback_to_images\",\n std::env::var(\"ASSET_FALLBACK_TO_IMAGES\")\n .ok()\n .map(|v| v.to_lowercase() == \"true\" || v == \"1\"),\n )?\n // YouTube\n .set_default(\"youtube.client_id\", \"\")?\n .set_override_option(\n \"youtube.client_id\",\n std::env::var(\"YOUTUBE_CLIENT_ID\").ok(),\n )?\n .set_default(\"youtube.client_secret\", \"\")?\n .set_override_option(\n \"youtube.client_secret\",\n std::env::var(\"YOUTUBE_CLIENT_SECRET\").ok(),\n )?\n .set_default(\"youtube.refresh_token\", \"\")?\n .set_override_option(\n \"youtube.refresh_token\",\n std::env::var(\"YOUTUBE_REFRESH_TOKEN\").ok(),\n )?\n // Channel\n .set_default(\"channel.name\", \"Excelsior Academy\")?\n .set_override_option(\"channel.name\", std::env::var(\"CHANNEL_NAME\").ok())?\n .set_default(\"channel.tagline\", \"Wisdom for the journey upward\")?\n .set_override_option(\"channel.tagline\", std::env::var(\"CHANNEL_TAGLINE\").ok())?\n // Content Strategy\n .set_default(\"content_strategy.target_duration_min\", 12)?\n .set_override_option(\n \"content_strategy.target_duration_min\",\n std::env::var(\"TARGET_DURATION_MIN\")\n .ok()\n .and_then(|v| v.parse::<i64>().ok()),\n )?\n .set_default(\"content_strategy.target_duration_max\", 20)?\n .set_override_option(\n \"content_strategy.target_duration_max\",\n std::env::var(\"TARGET_DURATION_MAX\")\n .ok()\n .and_then(|v| v.parse::<i64>().ok()),\n )?\n .set_default(\"content_strategy.videos_per_week\", 4)?\n .set_override_option(\n \"content_strategy.videos_per_week\",\n std::env::var(\"VIDEOS_PER_WEEK\")\n .ok()\n .and_then(|v| v.parse::<i64>().ok()),\n )?\n // Script Self-Improvement Loop\n .set_default(\"script_improvement.enabled\", true)?\n .set_override_option(\n \"script_improvement.enabled\",\n std::env::var(\"SCRIPT_IMPROVEMENT_ENABLED\")\n .ok()\n .map(|v| v.to_lowercase() == \"true\" || v == \"1\"),\n )?\n .set_default(\"script_improvement.candidate_count\", 3)?\n .set_override_option(\n \"script_improvement.candidate_count\",\n std::env::var(\"SCRIPT_IMPROVEMENT_CANDIDATES\")\n .ok()\n .and_then(|v| v.parse::<i64>().ok()),\n )?\n .set_default(\"script_improvement.quality_threshold\", 9.0)?\n .set_override_option(\n \"script_improvement.quality_threshold\",\n std::env::var(\"SCRIPT_IMPROVEMENT_THRESHOLD\")\n .ok()\n .and_then(|v| v.parse::<f64>().ok()),\n )?\n .set_default(\"script_improvement.max_iterations\", 10)?\n .set_override_option(\n \"script_improvement.max_iterations\",\n std::env::var(\"SCRIPT_IMPROVEMENT_MAX_ITERATIONS\")\n .ok()\n .and_then(|v| v.parse::<i64>().ok()),\n )?\n .set_default(\"script_improvement.stagnation_threshold\", 3)?\n .set_override_option(\n \"script_improvement.stagnation_threshold\",\n std::env::var(\"SCRIPT_IMPROVEMENT_STAGNATION_THRESHOLD\")\n .ok()\n .and_then(|v| v.parse::<i64>().ok()),\n )?\n .set_default(\"script_improvement.timeout_seconds\", 1800)?\n .set_override_option(\n \"script_improvement.timeout_seconds\",\n std::env::var(\"SCRIPT_IMPROVEMENT_TIMEOUT\")\n .ok()\n .and_then(|v| v.parse::<i64>().ok()),\n )?\n // Notifications\n .set_default(\"notifications.discord_webhook_url\", Option::<String>::None)?\n .set_override_option(\n \"notifications.discord_webhook_url\",\n std::env::var(\"DISCORD_WEBHOOK_URL\").ok(),\n )?\n .set_default(\"notifications.smtp_host\", Option::<String>::None)?\n .set_override_option(\"notifications.smtp_host\", std::env::var(\"SMTP_HOST\").ok())?\n .set_default(\"notifications.smtp_port\", Option::<u16>::None)?\n .set_override_option(\n \"notifications.smtp_port\",\n std::env::var(\"SMTP_PORT\")\n .ok()\n .and_then(|v| v.parse::<i64>().ok()),\n )?\n .set_default(\"notifications.smtp_user\", Option::<String>::None)?\n .set_override_option(\"notifications.smtp_user\", std::env::var(\"SMTP_USER\").ok())?\n .set_default(\"notifications.smtp_pass\", Option::<String>::None)?\n .set_override_option(\"notifications.smtp_pass\", std::env::var(\"SMTP_PASS\").ok())?\n .set_default(\"notifications.notification_email\", Option::<String>::None)?\n .set_override_option(\n \"notifications.notification_email\",\n std::env::var(\"NOTIFICATION_EMAIL\").ok(),\n )?\n // Control API\n .set_default(\"control_api_port\", 8080)?\n .set_override_option(\n \"control_api_port\",\n std::env::var(\"CONTROL_API_PORT\")\n .ok()\n .and_then(|v| v.parse::<i64>().ok()),\n )?\n .build()?;\n\n settings.try_deserialize()\n }\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n\n #[test]\n fn test_default_settings() {\n // This will use defaults since no .env is loaded in tests\n std::env::set_var(\"DATABASE_URL\", \"postgres://test:test@localhost/test\");\n let settings = Settings::from_env();\n assert!(settings.is_ok());\n }\n\n #[test]\n fn test_asset_config_loading() {\n std::env::set_var(\"DATABASE_URL\", \"postgres://test:test@localhost/test\");\n std::env::set_var(\"LEONARDO_API_KEY\", \"test_leonardo_key\");\n std::env::set_var(\"FREESOUND_API_KEY\", \"test_freesound_key\");\n std::env::set_var(\"ASSET_MIN_CLIPS_PER_SECTION\", \"5\");\n \n let settings = Settings::from_env().unwrap();\n assert_eq!(settings.assets.leonardo_api_key, Some(\"test_leonardo_key\".to_string()));\n assert_eq!(settings.assets.freesound_api_key, Some(\"test_freesound_key\".to_string()));\n assert_eq!(settings.assets.min_clips_per_section, 5);\n \n // Test backward compatibility with LEONARDOAI_API_KEY\n std::env::remove_var(\"LEONARDO_API_KEY\");\n std::env::set_var(\"LEONARDOAI_API_KEY\", \"test_leonardoai_key\");\n let settings2 = Settings::from_env().unwrap();\n assert_eq!(settings2.assets.leonardo_api_key, Some(\"test_leonardoai_key\".to_string()));\n \n // Clean up\n std::env::remove_var(\"LEONARDOAI_API_KEY\");\n std::env::remove_var(\"FREESOUND_API_KEY\");\n std::env::remove_var(\"ASSET_MIN_CLIPS_PER_SECTION\");\n }\n}\n"}, "5004666f07104286f11e55300db7756c111e1fc9": {"t": 1785757451.628691, "n": 16971, "h": "1773dcf09072bb46", "c": "//! Application settings loaded from environment variables\n\nuse serde::Deserialize;\n\n/// Root configuration structure\n#[derive(Debug, Clone, Deserialize)]\npub struct Settings {\n pub database: DatabaseConfig,\n pub s3: S3Config,\n pub llm: LlmConfig,\n pub tts: TtsConfig,\n pub assets: AssetConfig,\n pub youtube: YouTubeConfig,\n pub channel: ChannelConfig,\n pub content_strategy: ContentStrategyConfig,\n pub script_improvement: ScriptImprovementConfig,\n pub notifications: NotificationConfig,\n pub control_api_port: u16,\n}\n\nuse crate::nodes::tts::TTSProvider;\n\n#[derive(Debug, Clone, Deserialize)]\npub struct DatabaseConfig {\n pub url: String,\n}\n\n#[derive(Debug, Clone, Deserialize)]\npub struct S3Config {\n pub endpoint: String,\n pub access_key: String,\n pub secret_key: String,\n pub bucket: String,\n pub region: String,\n}\n\n#[derive(Debug, Clone, Deserialize)]\npub struct LlmConfig {\n pub deepseek_api_key: String,\n pub deepseek_base_url: String,\n pub gemini_api_key: String,\n /// Which provider to use by default: \"deepseek\" or \"gemini\"\n pub default_provider: String,\n}\n\n#[derive(Debug, Clone, Deserialize)]\npub struct TtsConfig {\n /// TTS provider: elevenlabs, qwen3, openai, coqui, piper\n pub provider: TTSProvider,\n\n // ElevenLabs settings\n pub elevenlabs_api_key: Option<String>,\n pub elevenlabs_voice_id: Option<String>,\n pub elevenlabs_model_id: Option<String>,\n\n // Qwen3-TTS settings (self-hosted, OpenAI-compatible)\n pub qwen3_api_url: Option<String>,\n pub qwen3_api_key: Option<String>,\n pub qwen3_voice: Option<String>,\n\n // OpenAI TTS settings\n pub openai_api_key: Option<String>,\n pub openai_voice: Option<String>,\n pub openai_model: Option<String>,\n\n // Local TTS settings (Coqui/Piper)\n pub local_model_path: Option<String>,\n pub local_speaker_id: Option<String>,\n\n // Common settings\n pub stability: f32,\n pub similarity_boost: f32,\n pub speed: f32,\n}\n\n#[derive(Debug, Clone, Deserialize)]\npub struct YouTubeConfig {\n pub client_id: String,\n pub client_secret: String,\n pub refresh_token: String,\n}\n\n#[derive(Debug, Clone, Deserialize)]\npub struct ChannelConfig {\n pub name: String,\n pub tagline: String,\n}\n\n#[derive(Debug, Clone, Deserialize)]\npub struct ContentStrategyConfig {\n pub target_duration_min: u32,\n pub target_duration_max: u32,\n pub videos_per_week: u32,\n}\n\n#[derive(Debug, Clone, Deserialize)]\npub struct NotificationConfig {\n pub discord_webhook_url: Option<String>,\n pub smtp_host: Option<String>,\n pub smtp_port: Option<u16>,\n pub smtp_user: Option<String>,\n pub smtp_pass: Option<String>,\n pub notification_email: Option<String>,\n}\n\n/// Pexels and AI asset services config\n#[derive(Debug, Clone, Deserialize)]\npub struct AssetConfig {\n pub pexels_api_key: String,\n pub leonardo_api_key: Option<String>,\n pub freesound_api_key: Option<String>,\n pub sd_api_url: Option<String>,\n pub sd_api_key: Option<String>,\n pub min_clips_per_section: u32,\n}\n\n/// Script self-improvement loop configuration\n#[derive(Debug, Clone, Deserialize)]\npub struct ScriptImprovementConfig {\n /// Enable the self-improvement loop\n pub enabled: bool,\n /// Number of initial script candidates to generate in parallel\n pub candidate_count: u32,\n /// Minimum score (1.0-10.0) to accept without refinement\n pub quality_threshold: f32,\n /// Maximum refinement iterations before accepting best available\n pub max_iterations: u32,\n /// Number of iterations without improvement before giving up early\n pub stagnation_threshold: u32,\n /// Total timeout for the entire self-improvement process (seconds)\n pub timeout_seconds: u32,\n}\n\nimpl Settings {\n /// Load settings from environment variables\n pub fn from_env() -> Result<Self, config::ConfigError> {\n dotenvy::dotenv().ok();\n\n let settings = config::Config::builder()\n // Database\n .set_default(\"database.url\", \"\")?\n .set_override_option(\n \"database.url\",\n std::env::var(\"DATABASE_URL\").ok(),\n )?\n // S3\n .set_default(\"s3.endpoint\", \"http://localhost:9000\")?\n .set_override_option(\"s3.endpoint\", std::env::var(\"S3_ENDPOINT\").ok())?\n .set_default(\"s3.access_key\", \"minioadmin\")?\n .set_override_option(\"s3.access_key\", std::env::var(\"S3_ACCESS_KEY\").ok())?\n .set_default(\"s3.secret_key\", \"minioadmin\")?\n .set_override_option(\"s3.secret_key\", std::env::var(\"S3_SECRET_KEY\").ok())?\n .set_default(\"s3.bucket\", \"animus-assets\")?\n .set_override_option(\"s3.bucket\", std::env::var(\"S3_BUCKET\").ok())?\n .set_default(\"s3.region\", \"us-east-1\")?\n .set_override_option(\"s3.region\", std::env::var(\"S3_REGION\").ok())?\n // LLM\n .set_default(\"llm.deepseek_api_key\", \"\")?\n .set_override_option(\n \"llm.deepseek_api_key\",\n std::env::var(\"DEEPSEEK_API_KEY\").ok(),\n )?\n .set_default(\"llm.deepseek_base_url\", \"https://api.deepseek.com\")?\n .set_override_option(\n \"llm.deepseek_base_url\",\n std::env::var(\"DEEPSEEK_BASE_URL\").ok(),\n )?\n .set_default(\"llm.gemini_api_key\", \"\")?\n .set_override_option(\"llm.gemini_api_key\", std::env::var(\"GEMINI_API_KEY\").ok())?\n .set_default(\"llm.default_provider\", \"deepseek\")?\n .set_override_option(\n \"llm.default_provider\",\n std::env::var(\"LLM_DEFAULT_PROVIDER\").ok(),\n )?\n // TTS\n .set_default(\"tts.provider\", \"elevenlabs\")?\n .set_override_option(\"tts.provider\", std::env::var(\"TTS_PROVIDER\").ok())?\n // ElevenLabs\n .set_default(\"tts.elevenlabs_api_key\", Option::<String>::None)?\n .set_override_option(\n \"tts.elevenlabs_api_key\",\n std::env::var(\"ELEVENLABS_API_KEY\").ok(),\n )?\n .set_default(\"tts.elevenlabs_voice_id\", Option::<String>::None)?\n .set_override_option(\n \"tts.elevenlabs_voice_id\",\n std::env::var(\"ELEVENLABS_VOICE_ID\").ok(),\n )?\n .set_default(\"tts.elevenlabs_model_id\", Option::<String>::None)?\n .set_override_option(\n \"tts.elevenlabs_model_id\",\n std::env::var(\"ELEVENLABS_MODEL_ID\").ok(),\n )?\n // Qwen3-TTS\n .set_default(\"tts.qwen3_api_url\", Option::<String>::None)?\n .set_override_option(\"tts.qwen3_api_url\", std::env::var(\"QWEN3_API_URL\").ok())?\n .set_default(\"tts.qwen3_api_key\", Option::<String>::None)?\n .set_override_option(\"tts.qwen3_api_key\", std::env::var(\"QWEN3_API_KEY\").ok())?\n .set_default(\"tts.qwen3_voice\", Option::<String>::None)?\n .set_override_option(\"tts.qwen3_voice\", std::env::var(\"QWEN3_VOICE\").ok())?\n // OpenAI TTS\n .set_default(\"tts.openai_api_key\", Option::<String>::None)?\n .set_override_option(\"tts.openai_api_key\", std::env::var(\"OPENAI_TTS_API_KEY\").ok())?\n .set_default(\"tts.openai_voice\", Option::<String>::None)?\n .set_override_option(\"tts.openai_voice\", std::env::var(\"OPENAI_TTS_VOICE\").ok())?\n .set_default(\"tts.openai_model\", Option::<String>::None)?\n .set_override_option(\"tts.openai_model\", std::env::var(\"OPENAI_TTS_MODEL\").ok())?\n // Local TTS\n .set_default(\"tts.local_model_path\", Option::<String>::None)?\n .set_override_option(\"tts.local_model_path\", std::env::var(\"LOCAL_TTS_MODEL_PATH\").ok())?\n .set_default(\"tts.local_speaker_id\", Option::<String>::None)?\n .set_override_option(\"tts.local_speaker_id\", std::env::var(\"LOCAL_TTS_SPEAKER_ID\").ok())?\n // Common TTS settings\n .set_default(\"tts.stability\", 0.5)?\n .set_default(\"tts.similarity_boost\", 0.75)?\n .set_default(\"tts.speed\", 1.0)?\n .set_override_option(\n \"tts.speed\",\n std::env::var(\"TTS_SPEED\").ok().and_then(|v| v.parse::<f64>().ok()),\n )?\n // Assets (Pexels, AI services, sound effects)\n .set_default(\"assets.pexels_api_key\", \"\")?\n .set_override_option(\n \"assets.pexels_api_key\",\n std::env::var(\"PEXELS_API_KEY\").ok(),\n )?\n .set_default(\"assets.leonardo_api_key\", Option::<String>::None)?\n .set_override_option(\n \"assets.leonardo_api_key\",\n std::env::var(\"LEONARDO_API_KEY\")\n .or_else(|_| std::env::var(\"LEONARDOAI_API_KEY\"))\n .ok()\n .filter(|s| !s.is_empty()),\n )?\n .set_default(\"assets.freesound_api_key\", Option::<String>::None)?\n .set_override_option(\n \"assets.freesound_api_key\",\n std::env::var(\"FREESOUND_API_KEY\").ok().filter(|s| !s.is_empty()),\n )?\n .set_default(\"assets.sd_api_url\", Option::<String>::None)?\n .set_override_option(\"assets.sd_api_url\", std::env::var(\"SD_API_URL\").ok().filter(|s| !s.is_empty()))?\n .set_default(\"assets.sd_api_key\", Option::<String>::None)?\n .set_override_option(\"assets.sd_api_key\", std::env::var(\"SD_API_KEY\").ok().filter(|s| !s.is_empty()))?\n .set_default(\"assets.min_clips_per_section\", 3)?\n .set_override_option(\n \"assets.min_clips_per_section\",\n std::env::var(\"ASSET_MIN_CLIPS_PER_SECTION\")\n .ok()\n .and_then(|v| v.parse::<i64>().ok()),\n )?\n // YouTube\n .set_default(\"youtube.client_id\", \"\")?\n .set_override_option(\n \"youtube.client_id\",\n std::env::var(\"YOUTUBE_CLIENT_ID\").ok(),\n )?\n .set_default(\"youtube.client_secret\", \"\")?\n .set_override_option(\n \"youtube.client_secret\",\n std::env::var(\"YOUTUBE_CLIENT_SECRET\").ok(),\n )?\n .set_default(\"youtube.refresh_token\", \"\")?\n .set_override_option(\n \"youtube.refresh_token\",\n std::env::var(\"YOUTUBE_REFRESH_TOKEN\").ok(),\n )?\n // Channel\n .set_default(\"channel.name\", \"Excelsior Academy\")?\n .set_override_option(\"channel.name\", std::env::var(\"CHANNEL_NAME\").ok())?\n .set_default(\"channel.tagline\", \"Wisdom for the journey upward\")?\n .set_override_option(\"channel.tagline\", std::env::var(\"CHANNEL_TAGLINE\").ok())?\n // Content Strategy\n .set_default(\"content_strategy.target_duration_min\", 12)?\n .set_override_option(\n \"content_strategy.target_duration_min\",\n std::env::var(\"TARGET_DURATION_MIN\")\n .ok()\n .and_then(|v| v.parse::<i64>().ok()),\n )?\n .set_default(\"content_strategy.target_duration_max\", 20)?\n .set_override_option(\n \"content_strategy.target_duration_max\",\n std::env::var(\"TARGET_DURATION_MAX\")\n .ok()\n .and_then(|v| v.parse::<i64>().ok()),\n )?\n .set_default(\"content_strategy.videos_per_week\", 4)?\n .set_override_option(\n \"content_strategy.videos_per_week\",\n std::env::var(\"VIDEOS_PER_WEEK\")\n .ok()\n .and_then(|v| v.parse::<i64>().ok()),\n )?\n // Script Self-Improvement Loop\n .set_default(\"script_improvement.enabled\", true)?\n .set_override_option(\n \"script_improvement.enabled\",\n std::env::var(\"SCRIPT_IMPROVEMENT_ENABLED\")\n .ok()\n .map(|v| v.to_lowercase() == \"true\" || v == \"1\"),\n )?\n .set_default(\"script_improvement.candidate_count\", 3)?\n .set_override_option(\n \"script_improvement.candidate_count\",\n std::env::var(\"SCRIPT_IMPROVEMENT_CANDIDATES\")\n .ok()\n .and_then(|v| v.parse::<i64>().ok()),\n )?\n .set_default(\"script_improvement.quality_threshold\", 9.0)?\n .set_override_option(\n \"script_improvement.quality_threshold\",\n std::env::var(\"SCRIPT_IMPROVEMENT_THRESHOLD\")\n .ok()\n .and_then(|v| v.parse::<f64>().ok()),\n )?\n .set_default(\"script_improvement.max_iterations\", 10)?\n .set_override_option(\n \"script_improvement.max_iterations\",\n std::env::var(\"SCRIPT_IMPROVEMENT_MAX_ITERATIONS\")\n .ok()\n .and_then(|v| v.parse::<i64>().ok()),\n )?\n .set_default(\"script_improvement.stagnation_threshold\", 3)?\n .set_override_option(\n \"script_improvement.stagnation_threshold\",\n std::env::var(\"SCRIPT_IMPROVEMENT_STAGNATION_THRESHOLD\")\n .ok()\n .and_then(|v| v.parse::<i64>().ok()),\n )?\n .set_default(\"script_improvement.timeout_seconds\", 1800)?\n .set_override_option(\n \"script_improvement.timeout_seconds\",\n std::env::var(\"SCRIPT_IMPROVEMENT_TIMEOUT\")\n .ok()\n .and_then(|v| v.parse::<i64>().ok()),\n )?\n // Notifications\n .set_default(\"notifications.discord_webhook_url\", Option::<String>::None)?\n .set_override_option(\n \"notifications.discord_webhook_url\",\n std::env::var(\"DISCORD_WEBHOOK_URL\").ok(),\n )?\n .set_default(\"notifications.smtp_host\", Option::<String>::None)?\n .set_override_option(\"notifications.smtp_host\", std::env::var(\"SMTP_HOST\").ok())?\n .set_default(\"notifications.smtp_port\", Option::<u16>::None)?\n .set_override_option(\n \"notifications.smtp_port\",\n std::env::var(\"SMTP_PORT\")\n .ok()\n .and_then(|v| v.parse::<i64>().ok()),\n )?\n .set_default(\"notifications.smtp_user\", Option::<String>::None)?\n .set_override_option(\"notifications.smtp_user\", std::env::var(\"SMTP_USER\").ok())?\n .set_default(\"notifications.smtp_pass\", Option::<String>::None)?\n .set_override_option(\"notifications.smtp_pass\", std::env::var(\"SMTP_PASS\").ok())?\n .set_default(\"notifications.notification_email\", Option::<String>::None)?\n .set_override_option(\n \"notifications.notification_email\",\n std::env::var(\"NOTIFICATION_EMAIL\").ok(),\n )?\n // Control API\n .set_default(\"control_api_port\", 8080)?\n .set_override_option(\n \"control_api_port\",\n std::env::var(\"CONTROL_API_PORT\")\n .ok()\n .and_then(|v| v.parse::<i64>().ok()),\n )?\n .build()?;\n\n settings.try_deserialize()\n }\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n\n #[test]\n fn test_default_settings() {\n // This will use defaults since no .env is loaded in tests\n std::env::set_var(\"DATABASE_URL\", \"postgres://test:test@localhost/test\");\n let settings = Settings::from_env();\n assert!(settings.is_ok());\n }\n\n #[test]\n fn test_asset_config_loading() {\n std::env::set_var(\"DATABASE_URL\", \"postgres://test:test@localhost/test\");\n std::env::set_var(\"LEONARDO_API_KEY\", \"test_leonardo_key\");\n std::env::set_var(\"FREESOUND_API_KEY\", \"test_freesound_key\");\n std::env::set_var(\"ASSET_MIN_CLIPS_PER_SECTION\", \"5\");\n \n let settings = Settings::from_env().unwrap();\n assert_eq!(settings.assets.leonardo_api_key, Some(\"test_leonardo_key\".to_string()));\n assert_eq!(settings.assets.freesound_api_key, Some(\"test_freesound_key\".to_string()));\n assert_eq!(settings.assets.min_clips_per_section, 5);\n \n // Test backward compatibility with LEONARDOAI_API_KEY\n std::env::remove_var(\"LEONARDO_API_KEY\");\n std::env::set_var(\"LEONARDOAI_API_KEY\", \"test_leonardoai_key\");\n let settings2 = Settings::from_env().unwrap();\n assert_eq!(settings2.assets.leonardo_api_key, Some(\"test_leonardoai_key\".to_string()));\n \n // Clean up\n std::env::remove_var(\"LEONARDOAI_API_KEY\");\n std::env::remove_var(\"FREESOUND_API_KEY\");\n std::env::remove_var(\"ASSET_MIN_CLIPS_PER_SECTION\");\n }\n}\n"}, "67a145530d45ea434aeb6b6d774a377c84c0788c": {"t": 1785757453.0726368, "n": 14683, "h": "0d5510f902a188bc", "c": "//! Application settings loaded from environment variables\n\nuse serde::Deserialize;\n\n/// Root configuration structure\n#[derive(Debug, Clone, Deserialize)]\npub struct Settings {\n pub database: DatabaseConfig,\n pub s3: S3Config,\n pub llm: LlmConfig,\n pub tts: TtsConfig,\n pub assets: AssetConfig,\n pub youtube: YouTubeConfig,\n pub channel: ChannelConfig,\n pub content_strategy: ContentStrategyConfig,\n pub script_improvement: ScriptImprovementConfig,\n pub notifications: NotificationConfig,\n pub control_api_port: u16,\n}\n\nuse crate::nodes::tts::TTSProvider;\n\n#[derive(Debug, Clone, Deserialize)]\npub struct DatabaseConfig {\n pub url: String,\n}\n\n#[derive(Debug, Clone, Deserialize)]\npub struct S3Config {\n pub endpoint: String,\n pub access_key: String,\n pub secret_key: String,\n pub bucket: String,\n pub region: String,\n}\n\n#[derive(Debug, Clone, Deserialize)]\npub struct LlmConfig {\n pub deepseek_api_key: String,\n pub deepseek_base_url: String,\n pub gemini_api_key: String,\n /// Which provider to use by default: \"deepseek\" or \"gemini\"\n pub default_provider: String,\n}\n\n#[derive(Debug, Clone, Deserialize)]\npub struct TtsConfig {\n /// TTS provider: elevenlabs, qwen3, openai, coqui, piper\n pub provider: TTSProvider,\n\n // ElevenLabs settings\n pub elevenlabs_api_key: Option<String>,\n pub elevenlabs_voice_id: Option<String>,\n pub elevenlabs_model_id: Option<String>,\n\n // Qwen3-TTS settings (self-hosted, OpenAI-compatible)\n pub qwen3_api_url: Option<String>,\n pub qwen3_api_key: Option<String>,\n pub qwen3_voice: Option<String>,\n\n // OpenAI TTS settings\n pub openai_api_key: Option<String>,\n pub openai_voice: Option<String>,\n pub openai_model: Option<String>,\n\n // Local TTS settings (Coqui/Piper)\n pub local_model_path: Option<String>,\n pub local_speaker_id: Option<String>,\n\n // Common settings\n pub stability: f32,\n pub similarity_boost: f32,\n pub speed: f32,\n}\n\n#[derive(Debug, Clone, Deserialize)]\npub struct YouTubeConfig {\n pub client_id: String,\n pub client_secret: String,\n pub refresh_token: String,\n}\n\n#[derive(Debug, Clone, Deserialize)]\npub struct ChannelConfig {\n pub name: String,\n pub tagline: String,\n}\n\n#[derive(Debug, Clone, Deserialize)]\npub struct ContentStrategyConfig {\n pub target_duration_min: u32,\n pub target_duration_max: u32,\n pub videos_per_week: u32,\n}\n\n#[derive(Debug, Clone, Deserialize)]\npub struct NotificationConfig {\n pub discord_webhook_url: Option<String>,\n pub smtp_host: Option<String>,\n pub smtp_port: Option<u16>,\n pub smtp_user: Option<String>,\n pub smtp_pass: Option<String>,\n pub notification_email: Option<String>,\n}\n\n/// Pexels and Stable Diffusion asset config\n#[derive(Debug, Clone, Deserialize)]\npub struct AssetConfig {\n pub pexels_api_key: String,\n pub sd_api_url: Option<String>,\n pub sd_api_key: Option<String>,\n}\n\n/// Script self-improvement loop configuration\n#[derive(Debug, Clone, Deserialize)]\npub struct ScriptImprovementConfig {\n /// Enable the self-improvement loop\n pub enabled: bool,\n /// Number of initial script candidates to generate in parallel\n pub candidate_count: u32,\n /// Minimum score (1.0-10.0) to accept without refinement\n pub quality_threshold: f32,\n /// Maximum refinement iterations before accepting best available\n pub max_iterations: u32,\n /// Number of iterations without improvement before giving up early\n pub stagnation_threshold: u32,\n /// Total timeout for the entire self-improvement process (seconds)\n pub timeout_seconds: u32,\n}\n\nimpl Settings {\n /// Load settings from environment variables\n pub fn from_env() -> Result<Self, config::ConfigError> {\n dotenvy::dotenv().ok();\n\n let settings = config::Config::builder()\n // Database\n .set_default(\"database.url\", \"\")?\n .set_override_option(\n \"database.url\",\n std::env::var(\"DATABASE_URL\").ok(),\n )?\n // S3\n .set_default(\"s3.endpoint\", \"http://localhost:9000\")?\n .set_override_option(\"s3.endpoint\", std::env::var(\"S3_ENDPOINT\").ok())?\n .set_default(\"s3.access_key\", \"minioadmin\")?\n .set_override_option(\"s3.access_key\", std::env::var(\"S3_ACCESS_KEY\").ok())?\n .set_default(\"s3.secret_key\", \"minioadmin\")?\n .set_override_option(\"s3.secret_key\", std::env::var(\"S3_SECRET_KEY\").ok())?\n .set_default(\"s3.bucket\", \"animus-assets\")?\n .set_override_option(\"s3.bucket\", std::env::var(\"S3_BUCKET\").ok())?\n .set_default(\"s3.region\", \"us-east-1\")?\n .set_override_option(\"s3.region\", std::env::var(\"S3_REGION\").ok())?\n // LLM\n .set_default(\"llm.deepseek_api_key\", \"\")?\n .set_override_option(\n \"llm.deepseek_api_key\",\n std::env::var(\"DEEPSEEK_API_KEY\").ok(),\n )?\n .set_default(\"llm.deepseek_base_url\", \"https://api.deepseek.com\")?\n .set_override_option(\n \"llm.deepseek_base_url\",\n std::env::var(\"DEEPSEEK_BASE_URL\").ok(),\n )?\n .set_default(\"llm.gemini_api_key\", \"\")?\n .set_override_option(\"llm.gemini_api_key\", std::env::var(\"GEMINI_API_KEY\").ok())?\n .set_default(\"llm.default_provider\", \"deepseek\")?\n .set_override_option(\n \"llm.default_provider\",\n std::env::var(\"LLM_DEFAULT_PROVIDER\").ok(),\n )?\n // TTS\n .set_default(\"tts.provider\", \"elevenlabs\")?\n .set_override_option(\"tts.provider\", std::env::var(\"TTS_PROVIDER\").ok())?\n // ElevenLabs\n .set_default(\"tts.elevenlabs_api_key\", Option::<String>::None)?\n .set_override_option(\n \"tts.elevenlabs_api_key\",\n std::env::var(\"ELEVENLABS_API_KEY\").ok(),\n )?\n .set_default(\"tts.elevenlabs_voice_id\", Option::<String>::None)?\n .set_override_option(\n \"tts.elevenlabs_voice_id\",\n std::env::var(\"ELEVENLABS_VOICE_ID\").ok(),\n )?\n .set_default(\"tts.elevenlabs_model_id\", Option::<String>::None)?\n .set_override_option(\n \"tts.elevenlabs_model_id\",\n std::env::var(\"ELEVENLABS_MODEL_ID\").ok(),\n )?\n // Qwen3-TTS\n .set_default(\"tts.qwen3_api_url\", Option::<String>::None)?\n .set_override_option(\"tts.qwen3_api_url\", std::env::var(\"QWEN3_API_URL\").ok())?\n .set_default(\"tts.qwen3_api_key\", Option::<String>::None)?\n .set_override_option(\"tts.qwen3_api_key\", std::env::var(\"QWEN3_API_KEY\").ok())?\n .set_default(\"tts.qwen3_voice\", Option::<String>::None)?\n .set_override_option(\"tts.qwen3_voice\", std::env::var(\"QWEN3_VOICE\").ok())?\n // OpenAI TTS\n .set_default(\"tts.openai_api_key\", Option::<String>::None)?\n .set_override_option(\"tts.openai_api_key\", std::env::var(\"OPENAI_TTS_API_KEY\").ok())?\n .set_default(\"tts.openai_voice\", Option::<String>::None)?\n .set_override_option(\"tts.openai_voice\", std::env::var(\"OPENAI_TTS_VOICE\").ok())?\n .set_default(\"tts.openai_model\", Option::<String>::None)?\n .set_override_option(\"tts.openai_model\", std::env::var(\"OPENAI_TTS_MODEL\").ok())?\n // Local TTS\n .set_default(\"tts.local_model_path\", Option::<String>::None)?\n .set_override_option(\"tts.local_model_path\", std::env::var(\"LOCAL_TTS_MODEL_PATH\").ok())?\n .set_default(\"tts.local_speaker_id\", Option::<String>::None)?\n .set_override_option(\"tts.local_speaker_id\", std::env::var(\"LOCAL_TTS_SPEAKER_ID\").ok())?\n // Common TTS settings\n .set_default(\"tts.stability\", 0.5)?\n .set_default(\"tts.similarity_boost\", 0.75)?\n .set_default(\"tts.speed\", 1.0)?\n .set_override_option(\n \"tts.speed\",\n std::env::var(\"TTS_SPEED\").ok().and_then(|v| v.parse::<f64>().ok()),\n )?\n // Assets (Pexels, Stable Diffusion)\n .set_default(\"assets.pexels_api_key\", \"\")?\n .set_override_option(\n \"assets.pexels_api_key\",\n std::env::var(\"PEXELS_API_KEY\").ok(),\n )?\n .set_default(\"assets.sd_api_url\", Option::<String>::None)?\n .set_override_option(\"assets.sd_api_url\", std::env::var(\"SD_API_URL\").ok())?\n .set_default(\"assets.sd_api_key\", Option::<String>::None)?\n .set_override_option(\"assets.sd_api_key\", std::env::var(\"SD_API_KEY\").ok())?\n // YouTube\n .set_default(\"youtube.client_id\", \"\")?\n .set_override_option(\n \"youtube.client_id\",\n std::env::var(\"YOUTUBE_CLIENT_ID\").ok(),\n )?\n .set_default(\"youtube.client_secret\", \"\")?\n .set_override_option(\n \"youtube.client_secret\",\n std::env::var(\"YOUTUBE_CLIENT_SECRET\").ok(),\n )?\n .set_default(\"youtube.refresh_token\", \"\")?\n .set_override_option(\n \"youtube.refresh_token\",\n std::env::var(\"YOUTUBE_REFRESH_TOKEN\").ok(),\n )?\n // Channel\n .set_default(\"channel.name\", \"Excelsior Academy\")?\n .set_override_option(\"channel.name\", std::env::var(\"CHANNEL_NAME\").ok())?\n .set_default(\"channel.tagline\", \"Wisdom for the journey upward\")?\n .set_override_option(\"channel.tagline\", std::env::var(\"CHANNEL_TAGLINE\").ok())?\n // Content Strategy\n .set_default(\"content_strategy.target_duration_min\", 12)?\n .set_override_option(\n \"content_strategy.target_duration_min\",\n std::env::var(\"TARGET_DURATION_MIN\")\n .ok()\n .and_then(|v| v.parse::<i64>().ok()),\n )?\n .set_default(\"content_strategy.target_duration_max\", 20)?\n .set_override_option(\n \"content_strategy.target_duration_max\",\n std::env::var(\"TARGET_DURATION_MAX\")\n .ok()\n .and_then(|v| v.parse::<i64>().ok()),\n )?\n .set_default(\"content_strategy.videos_per_week\", 4)?\n .set_override_option(\n \"content_strategy.videos_per_week\",\n std::env::var(\"VIDEOS_PER_WEEK\")\n .ok()\n .and_then(|v| v.parse::<i64>().ok()),\n )?\n // Script Self-Improvement Loop\n .set_default(\"script_improvement.enabled\", true)?\n .set_override_option(\n \"script_improvement.enabled\",\n std::env::var(\"SCRIPT_IMPROVEMENT_ENABLED\")\n .ok()\n .map(|v| v.to_lowercase() == \"true\" || v == \"1\"),\n )?\n .set_default(\"script_improvement.candidate_count\", 3)?\n .set_override_option(\n \"script_improvement.candidate_count\",\n std::env::var(\"SCRIPT_IMPROVEMENT_CANDIDATES\")\n .ok()\n .and_then(|v| v.parse::<i64>().ok()),\n )?\n .set_default(\"script_improvement.quality_threshold\", 9.0)?\n .set_override_option(\n \"script_improvement.quality_threshold\",\n std::env::var(\"SCRIPT_IMPROVEMENT_THRESHOLD\")\n .ok()\n .and_then(|v| v.parse::<f64>().ok()),\n )?\n .set_default(\"script_improvement.max_iterations\", 10)?\n .set_override_option(\n \"script_improvement.max_iterations\",\n std::env::var(\"SCRIPT_IMPROVEMENT_MAX_ITERATIONS\")\n .ok()\n .and_then(|v| v.parse::<i64>().ok()),\n )?\n .set_default(\"script_improvement.stagnation_threshold\", 3)?\n .set_override_option(\n \"script_improvement.stagnation_threshold\",\n std::env::var(\"SCRIPT_IMPROVEMENT_STAGNATION_THRESHOLD\")\n .ok()\n .and_then(|v| v.parse::<i64>().ok()),\n )?\n .set_default(\"script_improvement.timeout_seconds\", 1800)?\n .set_override_option(\n \"script_improvement.timeout_seconds\",\n std::env::var(\"SCRIPT_IMPROVEMENT_TIMEOUT\")\n .ok()\n .and_then(|v| v.parse::<i64>().ok()),\n )?\n // Notifications\n .set_default(\"notifications.discord_webhook_url\", Option::<String>::None)?\n .set_override_option(\n \"notifications.discord_webhook_url\",\n std::env::var(\"DISCORD_WEBHOOK_URL\").ok(),\n )?\n .set_default(\"notifications.smtp_host\", Option::<String>::None)?\n .set_override_option(\"notifications.smtp_host\", std::env::var(\"SMTP_HOST\").ok())?\n .set_default(\"notifications.smtp_port\", Option::<u16>::None)?\n .set_override_option(\n \"notifications.smtp_port\",\n std::env::var(\"SMTP_PORT\")\n .ok()\n .and_then(|v| v.parse::<i64>().ok()),\n )?\n .set_default(\"notifications.smtp_user\", Option::<String>::None)?\n .set_override_option(\"notifications.smtp_user\", std::env::var(\"SMTP_USER\").ok())?\n .set_default(\"notifications.smtp_pass\", Option::<String>::None)?\n .set_override_option(\"notifications.smtp_pass\", std::env::var(\"SMTP_PASS\").ok())?\n .set_default(\"notifications.notification_email\", Option::<String>::None)?\n .set_override_option(\n \"notifications.notification_email\",\n std::env::var(\"NOTIFICATION_EMAIL\").ok(),\n )?\n // Control API\n .set_default(\"control_api_port\", 8080)?\n .set_override_option(\n \"control_api_port\",\n std::env::var(\"CONTROL_API_PORT\")\n .ok()\n .and_then(|v| v.parse::<i64>().ok()),\n )?\n .build()?;\n\n settings.try_deserialize()\n }\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n\n #[test]\n fn test_default_settings() {\n // This will use defaults since no .env is loaded in tests\n std::env::set_var(\"DATABASE_URL\", \"postgres://test:test@localhost/test\");\n let settings = Settings::from_env();\n assert!(settings.is_ok());\n }\n}\n"}, "a5ee01fc3041ebc7baa09ba01f06872796047aa4": {"t": 1785757454.7104847, "n": 14239, "h": "edfa8e77a8813920", "c": "//! Application settings loaded from environment variables\n\nuse serde::Deserialize;\n\n/// Root configuration structure\n#[derive(Debug, Clone, Deserialize)]\npub struct Settings {\n pub database: DatabaseConfig,\n pub s3: S3Config,\n pub llm: LlmConfig,\n pub tts: TtsConfig,\n pub assets: AssetConfig,\n pub youtube: YouTubeConfig,\n pub channel: ChannelConfig,\n pub content_strategy: ContentStrategyConfig,\n pub script_improvement: ScriptImprovementConfig,\n pub notifications: NotificationConfig,\n pub control_api_port: u16,\n}\n\nuse crate::nodes::tts::TTSProvider;\n\n#[derive(Debug, Clone, Deserialize)]\npub struct DatabaseConfig {\n pub url: String,\n}\n\n#[derive(Debug, Clone, Deserialize)]\npub struct S3Config {\n pub endpoint: String,\n pub access_key: String,\n pub secret_key: String,\n pub bucket: String,\n pub region: String,\n}\n\n#[derive(Debug, Clone, Deserialize)]\npub struct LlmConfig {\n pub deepseek_api_key: String,\n pub deepseek_base_url: String,\n pub gemini_api_key: String,\n /// Which provider to use by default: \"deepseek\" or \"gemini\"\n pub default_provider: String,\n}\n\n#[derive(Debug, Clone, Deserialize)]\npub struct TtsConfig {\n /// TTS provider: elevenlabs, qwen3, openai, coqui, piper\n pub provider: TTSProvider,\n\n // ElevenLabs settings\n pub elevenlabs_api_key: Option<String>,\n pub elevenlabs_voice_id: Option<String>,\n pub elevenlabs_model_id: Option<String>,\n\n // Qwen3-TTS settings (self-hosted, OpenAI-compatible)\n pub qwen3_api_url: Option<String>,\n pub qwen3_api_key: Option<String>,\n pub qwen3_voice: Option<String>,\n\n // OpenAI TTS settings\n pub openai_api_key: Option<String>,\n pub openai_voice: Option<String>,\n pub openai_model: Option<String>,\n\n // Local TTS settings (Coqui/Piper)\n pub local_model_path: Option<String>,\n pub local_speaker_id: Option<String>,\n\n // Common settings\n pub stability: f32,\n pub similarity_boost: f32,\n pub speed: f32,\n}\n\n#[derive(Debug, Clone, Deserialize)]\npub struct YouTubeConfig {\n pub client_id: String,\n pub client_secret: String,\n pub refresh_token: String,\n}\n\n#[derive(Debug, Clone, Deserialize)]\npub struct ChannelConfig {\n pub name: String,\n pub tagline: String,\n}\n\n#[derive(Debug, Clone, Deserialize)]\npub struct ContentStrategyConfig {\n pub target_duration_min: u32,\n pub target_duration_max: u32,\n pub videos_per_week: u32,\n}\n\n#[derive(Debug, Clone, Deserialize)]\npub struct NotificationConfig {\n pub discord_webhook_url: Option<String>,\n pub smtp_host: Option<String>,\n pub smtp_port: Option<u16>,\n pub smtp_user: Option<String>,\n pub smtp_pass: Option<String>,\n pub notification_email: Option<String>,\n}\n\n/// Pexels and Stable Diffusion asset config\n#[derive(Debug, Clone, Deserialize)]\npub struct AssetConfig {\n pub pexels_api_key: String,\n pub sd_api_url: Option<String>,\n pub sd_api_key: Option<String>,\n}\n\n/// Script self-improvement loop configuration\n#[derive(Debug, Clone, Deserialize)]\npub struct ScriptImprovementConfig {\n /// Enable the self-improvement loop\n pub enabled: bool,\n /// Number of initial script candidates to generate in parallel\n pub candidate_count: u32,\n /// Minimum score (1.0-10.0) to accept without refinement\n pub quality_threshold: f32,\n /// Maximum refinement iterations before accepting best available\n pub max_iterations: u32,\n /// Total timeout for the entire self-improvement process (seconds)\n pub timeout_seconds: u32,\n}\n\nimpl Settings {\n /// Load settings from environment variables\n pub fn from_env() -> Result<Self, config::ConfigError> {\n dotenvy::dotenv().ok();\n\n let settings = config::Config::builder()\n // Database\n .set_default(\"database.url\", \"\")?\n .set_override_option(\n \"database.url\",\n std::env::var(\"DATABASE_URL\").ok(),\n )?\n // S3\n .set_default(\"s3.endpoint\", \"http://localhost:9000\")?\n .set_override_option(\"s3.endpoint\", std::env::var(\"S3_ENDPOINT\").ok())?\n .set_default(\"s3.access_key\", \"minioadmin\")?\n .set_override_option(\"s3.access_key\", std::env::var(\"S3_ACCESS_KEY\").ok())?\n .set_default(\"s3.secret_key\", \"minioadmin\")?\n .set_override_option(\"s3.secret_key\", std::env::var(\"S3_SECRET_KEY\").ok())?\n .set_default(\"s3.bucket\", \"animus-assets\")?\n .set_override_option(\"s3.bucket\", std::env::var(\"S3_BUCKET\").ok())?\n .set_default(\"s3.region\", \"us-east-1\")?\n .set_override_option(\"s3.region\", std::env::var(\"S3_REGION\").ok())?\n // LLM\n .set_default(\"llm.deepseek_api_key\", \"\")?\n .set_override_option(\n \"llm.deepseek_api_key\",\n std::env::var(\"DEEPSEEK_API_KEY\").ok(),\n )?\n .set_default(\"llm.deepseek_base_url\", \"https://api.deepseek.com\")?\n .set_override_option(\n \"llm.deepseek_base_url\",\n std::env::var(\"DEEPSEEK_BASE_URL\").ok(),\n )?\n .set_default(\"llm.gemini_api_key\", \"\")?\n .set_override_option(\"llm.gemini_api_key\", std::env::var(\"GEMINI_API_KEY\").ok())?\n .set_default(\"llm.default_provider\", \"deepseek\")?\n .set_override_option(\n \"llm.default_provider\",\n std::env::var(\"LLM_DEFAULT_PROVIDER\").ok(),\n )?\n // TTS\n .set_default(\"tts.provider\", \"elevenlabs\")?\n .set_override_option(\"tts.provider\", std::env::var(\"TTS_PROVIDER\").ok())?\n // ElevenLabs\n .set_default(\"tts.elevenlabs_api_key\", Option::<String>::None)?\n .set_override_option(\n \"tts.elevenlabs_api_key\",\n std::env::var(\"ELEVENLABS_API_KEY\").ok(),\n )?\n .set_default(\"tts.elevenlabs_voice_id\", Option::<String>::None)?\n .set_override_option(\n \"tts.elevenlabs_voice_id\",\n std::env::var(\"ELEVENLABS_VOICE_ID\").ok(),\n )?\n .set_default(\"tts.elevenlabs_model_id\", Option::<String>::None)?\n .set_override_option(\n \"tts.elevenlabs_model_id\",\n std::env::var(\"ELEVENLABS_MODEL_ID\").ok(),\n )?\n // Qwen3-TTS\n .set_default(\"tts.qwen3_api_url\", Option::<String>::None)?\n .set_override_option(\"tts.qwen3_api_url\", std::env::var(\"QWEN3_API_URL\").ok())?\n .set_default(\"tts.qwen3_api_key\", Option::<String>::None)?\n .set_override_option(\"tts.qwen3_api_key\", std::env::var(\"QWEN3_API_KEY\").ok())?\n .set_default(\"tts.qwen3_voice\", Option::<String>::None)?\n .set_override_option(\"tts.qwen3_voice\", std::env::var(\"QWEN3_VOICE\").ok())?\n // OpenAI TTS\n .set_default(\"tts.openai_api_key\", Option::<String>::None)?\n .set_override_option(\"tts.openai_api_key\", std::env::var(\"OPENAI_TTS_API_KEY\").ok())?\n .set_default(\"tts.openai_voice\", Option::<String>::None)?\n .set_override_option(\"tts.openai_voice\", std::env::var(\"OPENAI_TTS_VOICE\").ok())?\n .set_default(\"tts.openai_model\", Option::<String>::None)?\n .set_override_option(\"tts.openai_model\", std::env::var(\"OPENAI_TTS_MODEL\").ok())?\n // Local TTS\n .set_default(\"tts.local_model_path\", Option::<String>::None)?\n .set_override_option(\"tts.local_model_path\", std::env::var(\"LOCAL_TTS_MODEL_PATH\").ok())?\n .set_default(\"tts.local_speaker_id\", Option::<String>::None)?\n .set_override_option(\"tts.local_speaker_id\", std::env::var(\"LOCAL_TTS_SPEAKER_ID\").ok())?\n // Common TTS settings\n .set_default(\"tts.stability\", 0.5)?\n .set_default(\"tts.similarity_boost\", 0.75)?\n .set_default(\"tts.speed\", 1.0)?\n .set_override_option(\n \"tts.speed\",\n std::env::var(\"TTS_SPEED\").ok().and_then(|v| v.parse::<f64>().ok()),\n )?\n // Assets (Pexels, Stable Diffusion)\n .set_default(\"assets.pexels_api_key\", \"\")?\n .set_override_option(\n \"assets.pexels_api_key\",\n std::env::var(\"PEXELS_API_KEY\").ok(),\n )?\n .set_default(\"assets.sd_api_url\", Option::<String>::None)?\n .set_override_option(\"assets.sd_api_url\", std::env::var(\"SD_API_URL\").ok())?\n .set_default(\"assets.sd_api_key\", Option::<String>::None)?\n .set_override_option(\"assets.sd_api_key\", std::env::var(\"SD_API_KEY\").ok())?\n // YouTube\n .set_default(\"youtube.client_id\", \"\")?\n .set_override_option(\n \"youtube.client_id\",\n std::env::var(\"YOUTUBE_CLIENT_ID\").ok(),\n )?\n .set_default(\"youtube.client_secret\", \"\")?\n .set_override_option(\n \"youtube.client_secret\",\n std::env::var(\"YOUTUBE_CLIENT_SECRET\").ok(),\n )?\n .set_default(\"youtube.refresh_token\", \"\")?\n .set_override_option(\n \"youtube.refresh_token\",\n std::env::var(\"YOUTUBE_REFRESH_TOKEN\").ok(),\n )?\n // Channel\n .set_default(\"channel.name\", \"Excelsior Academy\")?\n .set_override_option(\"channel.name\", std::env::var(\"CHANNEL_NAME\").ok())?\n .set_default(\"channel.tagline\", \"Wisdom for the journey upward\")?\n .set_override_option(\"channel.tagline\", std::env::var(\"CHANNEL_TAGLINE\").ok())?\n // Content Strategy\n .set_default(\"content_strategy.target_duration_min\", 12)?\n .set_override_option(\n \"content_strategy.target_duration_min\",\n std::env::var(\"TARGET_DURATION_MIN\")\n .ok()\n .and_then(|v| v.parse::<i64>().ok()),\n )?\n .set_default(\"content_strategy.target_duration_max\", 20)?\n .set_override_option(\n \"content_strategy.target_duration_max\",\n std::env::var(\"TARGET_DURATION_MAX\")\n .ok()\n .and_then(|v| v.parse::<i64>().ok()),\n )?\n .set_default(\"content_strategy.videos_per_week\", 4)?\n .set_override_option(\n \"content_strategy.videos_per_week\",\n std::env::var(\"VIDEOS_PER_WEEK\")\n .ok()\n .and_then(|v| v.parse::<i64>().ok()),\n )?\n // Script Self-Improvement Loop\n .set_default(\"script_improvement.enabled\", true)?\n .set_override_option(\n \"script_improvement.enabled\",\n std::env::var(\"SCRIPT_IMPROVEMENT_ENABLED\")\n .ok()\n .map(|v| v.to_lowercase() == \"true\" || v == \"1\"),\n )?\n .set_default(\"script_improvement.candidate_count\", 3)?\n .set_override_option(\n \"script_improvement.candidate_count\",\n std::env::var(\"SCRIPT_IMPROVEMENT_CANDIDATES\")\n .ok()\n .and_then(|v| v.parse::<i64>().ok()),\n )?\n .set_default(\"script_improvement.quality_threshold\", 8.0)?\n .set_override_option(\n \"script_improvement.quality_threshold\",\n std::env::var(\"SCRIPT_IMPROVEMENT_THRESHOLD\")\n .ok()\n .and_then(|v| v.parse::<f64>().ok()),\n )?\n .set_default(\"script_improvement.max_iterations\", 10)?\n .set_override_option(\n \"script_improvement.max_iterations\",\n std::env::var(\"SCRIPT_IMPROVEMENT_MAX_ITERATIONS\")\n .ok()\n .and_then(|v| v.parse::<i64>().ok()),\n )?\n .set_default(\"script_improvement.timeout_seconds\", 1800)?\n .set_override_option(\n \"script_improvement.timeout_seconds\",\n std::env::var(\"SCRIPT_IMPROVEMENT_TIMEOUT\")\n .ok()\n .and_then(|v| v.parse::<i64>().ok()),\n )?\n // Notifications\n .set_default(\"notifications.discord_webhook_url\", Option::<String>::None)?\n .set_override_option(\n \"notifications.discord_webhook_url\",\n std::env::var(\"DISCORD_WEBHOOK_URL\").ok(),\n )?\n .set_default(\"notifications.smtp_host\", Option::<String>::None)?\n .set_override_option(\"notifications.smtp_host\", std::env::var(\"SMTP_HOST\").ok())?\n .set_default(\"notifications.smtp_port\", Option::<u16>::None)?\n .set_override_option(\n \"notifications.smtp_port\",\n std::env::var(\"SMTP_PORT\")\n .ok()\n .and_then(|v| v.parse::<i64>().ok()),\n )?\n .set_default(\"notifications.smtp_user\", Option::<String>::None)?\n .set_override_option(\"notifications.smtp_user\", std::env::var(\"SMTP_USER\").ok())?\n .set_default(\"notifications.smtp_pass\", Option::<String>::None)?\n .set_override_option(\"notifications.smtp_pass\", std::env::var(\"SMTP_PASS\").ok())?\n .set_default(\"notifications.notification_email\", Option::<String>::None)?\n .set_override_option(\n \"notifications.notification_email\",\n std::env::var(\"NOTIFICATION_EMAIL\").ok(),\n )?\n // Control API\n .set_default(\"control_api_port\", 8080)?\n .set_override_option(\n \"control_api_port\",\n std::env::var(\"CONTROL_API_PORT\")\n .ok()\n .and_then(|v| v.parse::<i64>().ok()),\n )?\n .build()?;\n\n settings.try_deserialize()\n }\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n\n #[test]\n fn test_default_settings() {\n // This will use defaults since no .env is loaded in tests\n std::env::set_var(\"DATABASE_URL\", \"postgres://test:test@localhost/test\");\n let settings = Settings::from_env();\n assert!(settings.is_ok());\n }\n}\n"}, "2b4f82c62e6a307c667581f4cff1a50842d8bf57": {"t": 1785757456.0133739, "n": 14259, "h": "0db8db90900e48d9", "c": "//! Application settings loaded from environment variables\n\nuse serde::Deserialize;\n\n/// Root configuration structure\n#[derive(Debug, Clone, Deserialize)]\npub struct Settings {\n pub database: DatabaseConfig,\n pub s3: S3Config,\n pub llm: LlmConfig,\n pub tts: TtsConfig,\n pub assets: AssetConfig,\n pub youtube: YouTubeConfig,\n pub channel: ChannelConfig,\n pub content_strategy: ContentStrategyConfig,\n pub script_improvement: ScriptImprovementConfig,\n pub notifications: NotificationConfig,\n pub control_api_port: u16,\n}\n\nuse crate::nodes::tts::TTSProvider;\n\n#[derive(Debug, Clone, Deserialize)]\npub struct DatabaseConfig {\n pub url: String,\n}\n\n#[derive(Debug, Clone, Deserialize)]\npub struct S3Config {\n pub endpoint: String,\n pub access_key: String,\n pub secret_key: String,\n pub bucket: String,\n pub region: String,\n}\n\n#[derive(Debug, Clone, Deserialize)]\npub struct LlmConfig {\n pub deepseek_api_key: String,\n pub deepseek_base_url: String,\n pub gemini_api_key: String,\n /// Which provider to use by default: \"deepseek\" or \"gemini\"\n pub default_provider: String,\n}\n\n#[derive(Debug, Clone, Deserialize)]\npub struct TtsConfig {\n /// TTS provider: elevenlabs, qwen3, openai, coqui, piper\n pub provider: TTSProvider,\n \n // ElevenLabs settings\n pub elevenlabs_api_key: Option<String>,\n pub elevenlabs_voice_id: Option<String>,\n pub elevenlabs_model_id: Option<String>,\n \n // Qwen3-TTS settings (self-hosted, OpenAI-compatible)\n pub qwen3_api_url: Option<String>,\n pub qwen3_api_key: Option<String>,\n pub qwen3_voice: Option<String>,\n \n // OpenAI TTS settings\n pub openai_api_key: Option<String>,\n pub openai_voice: Option<String>,\n pub openai_model: Option<String>,\n \n // Local TTS settings (Coqui/Piper)\n pub local_model_path: Option<String>,\n pub local_speaker_id: Option<String>,\n \n // Common settings\n pub stability: f32,\n pub similarity_boost: f32,\n pub speed: f32,\n}\n\n#[derive(Debug, Clone, Deserialize)]\npub struct YouTubeConfig {\n pub client_id: String,\n pub client_secret: String,\n pub refresh_token: String,\n}\n\n#[derive(Debug, Clone, Deserialize)]\npub struct ChannelConfig {\n pub name: String,\n pub tagline: String,\n}\n\n#[derive(Debug, Clone, Deserialize)]\npub struct ContentStrategyConfig {\n pub target_duration_min: u32,\n pub target_duration_max: u32,\n pub videos_per_week: u32,\n}\n\n#[derive(Debug, Clone, Deserialize)]\npub struct NotificationConfig {\n pub discord_webhook_url: Option<String>,\n pub smtp_host: Option<String>,\n pub smtp_port: Option<u16>,\n pub smtp_user: Option<String>,\n pub smtp_pass: Option<String>,\n pub notification_email: Option<String>,\n}\n\n/// Pexels and Stable Diffusion asset config\n#[derive(Debug, Clone, Deserialize)]\npub struct AssetConfig {\n pub pexels_api_key: String,\n pub sd_api_url: Option<String>,\n pub sd_api_key: Option<String>,\n}\n\n/// Script self-improvement loop configuration\n#[derive(Debug, Clone, Deserialize)]\npub struct ScriptImprovementConfig {\n /// Enable the self-improvement loop\n pub enabled: bool,\n /// Number of initial script candidates to generate in parallel\n pub candidate_count: u32,\n /// Minimum score (1.0-10.0) to accept without refinement\n pub quality_threshold: f32,\n /// Maximum refinement iterations before accepting best available\n pub max_iterations: u32,\n /// Total timeout for the entire self-improvement process (seconds)\n pub timeout_seconds: u32,\n}\n\nimpl Settings {\n /// Load settings from environment variables\n pub fn from_env() -> Result<Self, config::ConfigError> {\n dotenvy::dotenv().ok();\n\n let settings = config::Config::builder()\n // Database\n .set_default(\"database.url\", \"\")?\n .set_override_option(\n \"database.url\",\n std::env::var(\"DATABASE_URL\").ok(),\n )?\n // S3\n .set_default(\"s3.endpoint\", \"http://localhost:9000\")?\n .set_override_option(\"s3.endpoint\", std::env::var(\"S3_ENDPOINT\").ok())?\n .set_default(\"s3.access_key\", \"minioadmin\")?\n .set_override_option(\"s3.access_key\", std::env::var(\"S3_ACCESS_KEY\").ok())?\n .set_default(\"s3.secret_key\", \"minioadmin\")?\n .set_override_option(\"s3.secret_key\", std::env::var(\"S3_SECRET_KEY\").ok())?\n .set_default(\"s3.bucket\", \"animus-assets\")?\n .set_override_option(\"s3.bucket\", std::env::var(\"S3_BUCKET\").ok())?\n .set_default(\"s3.region\", \"us-east-1\")?\n .set_override_option(\"s3.region\", std::env::var(\"S3_REGION\").ok())?\n // LLM\n .set_default(\"llm.deepseek_api_key\", \"\")?\n .set_override_option(\n \"llm.deepseek_api_key\",\n std::env::var(\"DEEPSEEK_API_KEY\").ok(),\n )?\n .set_default(\"llm.deepseek_base_url\", \"https://api.deepseek.com\")?\n .set_override_option(\n \"llm.deepseek_base_url\",\n std::env::var(\"DEEPSEEK_BASE_URL\").ok(),\n )?\n .set_default(\"llm.gemini_api_key\", \"\")?\n .set_override_option(\"llm.gemini_api_key\", std::env::var(\"GEMINI_API_KEY\").ok())?\n .set_default(\"llm.default_provider\", \"deepseek\")?\n .set_override_option(\n \"llm.default_provider\",\n std::env::var(\"LLM_DEFAULT_PROVIDER\").ok(),\n )?\n // TTS\n .set_default(\"tts.provider\", \"elevenlabs\")?\n .set_override_option(\"tts.provider\", std::env::var(\"TTS_PROVIDER\").ok())?\n // ElevenLabs\n .set_default(\"tts.elevenlabs_api_key\", Option::<String>::None)?\n .set_override_option(\n \"tts.elevenlabs_api_key\",\n std::env::var(\"ELEVENLABS_API_KEY\").ok(),\n )?\n .set_default(\"tts.elevenlabs_voice_id\", Option::<String>::None)?\n .set_override_option(\n \"tts.elevenlabs_voice_id\",\n std::env::var(\"ELEVENLABS_VOICE_ID\").ok(),\n )?\n .set_default(\"tts.elevenlabs_model_id\", Option::<String>::None)?\n .set_override_option(\n \"tts.elevenlabs_model_id\",\n std::env::var(\"ELEVENLABS_MODEL_ID\").ok(),\n )?\n // Qwen3-TTS\n .set_default(\"tts.qwen3_api_url\", Option::<String>::None)?\n .set_override_option(\"tts.qwen3_api_url\", std::env::var(\"QWEN3_API_URL\").ok())?\n .set_default(\"tts.qwen3_api_key\", Option::<String>::None)?\n .set_override_option(\"tts.qwen3_api_key\", std::env::var(\"QWEN3_API_KEY\").ok())?\n .set_default(\"tts.qwen3_voice\", Option::<String>::None)?\n .set_override_option(\"tts.qwen3_voice\", std::env::var(\"QWEN3_VOICE\").ok())?\n // OpenAI TTS\n .set_default(\"tts.openai_api_key\", Option::<String>::None)?\n .set_override_option(\"tts.openai_api_key\", std::env::var(\"OPENAI_TTS_API_KEY\").ok())?\n .set_default(\"tts.openai_voice\", Option::<String>::None)?\n .set_override_option(\"tts.openai_voice\", std::env::var(\"OPENAI_TTS_VOICE\").ok())?\n .set_default(\"tts.openai_model\", Option::<String>::None)?\n .set_override_option(\"tts.openai_model\", std::env::var(\"OPENAI_TTS_MODEL\").ok())?\n // Local TTS\n .set_default(\"tts.local_model_path\", Option::<String>::None)?\n .set_override_option(\"tts.local_model_path\", std::env::var(\"LOCAL_TTS_MODEL_PATH\").ok())?\n .set_default(\"tts.local_speaker_id\", Option::<String>::None)?\n .set_override_option(\"tts.local_speaker_id\", std::env::var(\"LOCAL_TTS_SPEAKER_ID\").ok())?\n // Common TTS settings\n .set_default(\"tts.stability\", 0.5)?\n .set_default(\"tts.similarity_boost\", 0.75)?\n .set_default(\"tts.speed\", 1.0)?\n .set_override_option(\n \"tts.speed\",\n std::env::var(\"TTS_SPEED\").ok().and_then(|v| v.parse::<f64>().ok()),\n )?\n // Assets (Pexels, Stable Diffusion)\n .set_default(\"assets.pexels_api_key\", \"\")?\n .set_override_option(\n \"assets.pexels_api_key\",\n std::env::var(\"PEXELS_API_KEY\").ok(),\n )?\n .set_default(\"assets.sd_api_url\", Option::<String>::None)?\n .set_override_option(\"assets.sd_api_url\", std::env::var(\"SD_API_URL\").ok())?\n .set_default(\"assets.sd_api_key\", Option::<String>::None)?\n .set_override_option(\"assets.sd_api_key\", std::env::var(\"SD_API_KEY\").ok())?\n // YouTube\n .set_default(\"youtube.client_id\", \"\")?\n .set_override_option(\n \"youtube.client_id\",\n std::env::var(\"YOUTUBE_CLIENT_ID\").ok(),\n )?\n .set_default(\"youtube.client_secret\", \"\")?\n .set_override_option(\n \"youtube.client_secret\",\n std::env::var(\"YOUTUBE_CLIENT_SECRET\").ok(),\n )?\n .set_default(\"youtube.refresh_token\", \"\")?\n .set_override_option(\n \"youtube.refresh_token\",\n std::env::var(\"YOUTUBE_REFRESH_TOKEN\").ok(),\n )?\n // Channel\n .set_default(\"channel.name\", \"Excelsior Academy\")?\n .set_override_option(\"channel.name\", std::env::var(\"CHANNEL_NAME\").ok())?\n .set_default(\"channel.tagline\", \"Wisdom for the journey upward\")?\n .set_override_option(\"channel.tagline\", std::env::var(\"CHANNEL_TAGLINE\").ok())?\n // Content Strategy\n .set_default(\"content_strategy.target_duration_min\", 12)?\n .set_override_option(\n \"content_strategy.target_duration_min\",\n std::env::var(\"TARGET_DURATION_MIN\")\n .ok()\n .and_then(|v| v.parse::<i64>().ok()),\n )?\n .set_default(\"content_strategy.target_duration_max\", 20)?\n .set_override_option(\n \"content_strategy.target_duration_max\",\n std::env::var(\"TARGET_DURATION_MAX\")\n .ok()\n .and_then(|v| v.parse::<i64>().ok()),\n )?\n .set_default(\"content_strategy.videos_per_week\", 4)?\n .set_override_option(\n \"content_strategy.videos_per_week\",\n std::env::var(\"VIDEOS_PER_WEEK\")\n .ok()\n .and_then(|v| v.parse::<i64>().ok()),\n )?\n // Script Self-Improvement Loop\n .set_default(\"script_improvement.enabled\", true)?\n .set_override_option(\n \"script_improvement.enabled\",\n std::env::var(\"SCRIPT_IMPROVEMENT_ENABLED\")\n .ok()\n .map(|v| v.to_lowercase() == \"true\" || v == \"1\"),\n )?\n .set_default(\"script_improvement.candidate_count\", 3)?\n .set_override_option(\n \"script_improvement.candidate_count\",\n std::env::var(\"SCRIPT_IMPROVEMENT_CANDIDATES\")\n .ok()\n .and_then(|v| v.parse::<i64>().ok()),\n )?\n .set_default(\"script_improvement.quality_threshold\", 8.0)?\n .set_override_option(\n \"script_improvement.quality_threshold\",\n std::env::var(\"SCRIPT_IMPROVEMENT_THRESHOLD\")\n .ok()\n .and_then(|v| v.parse::<f64>().ok()),\n )?\n .set_default(\"script_improvement.max_iterations\", 10)?\n .set_override_option(\n \"script_improvement.max_iterations\",\n std::env::var(\"SCRIPT_IMPROVEMENT_MAX_ITERATIONS\")\n .ok()\n .and_then(|v| v.parse::<i64>().ok()),\n )?\n .set_default(\"script_improvement.timeout_seconds\", 1800)?\n .set_override_option(\n \"script_improvement.timeout_seconds\",\n std::env::var(\"SCRIPT_IMPROVEMENT_TIMEOUT\")\n .ok()\n .and_then(|v| v.parse::<i64>().ok()),\n )?\n // Notifications\n .set_default(\"notifications.discord_webhook_url\", Option::<String>::None)?\n .set_override_option(\n \"notifications.discord_webhook_url\",\n std::env::var(\"DISCORD_WEBHOOK_URL\").ok(),\n )?\n .set_default(\"notifications.smtp_host\", Option::<String>::None)?\n .set_override_option(\"notifications.smtp_host\", std::env::var(\"SMTP_HOST\").ok())?\n .set_default(\"notifications.smtp_port\", Option::<u16>::None)?\n .set_override_option(\n \"notifications.smtp_port\",\n std::env::var(\"SMTP_PORT\")\n .ok()\n .and_then(|v| v.parse::<i64>().ok()),\n )?\n .set_default(\"notifications.smtp_user\", Option::<String>::None)?\n .set_override_option(\"notifications.smtp_user\", std::env::var(\"SMTP_USER\").ok())?\n .set_default(\"notifications.smtp_pass\", Option::<String>::None)?\n .set_override_option(\"notifications.smtp_pass\", std::env::var(\"SMTP_PASS\").ok())?\n .set_default(\"notifications.notification_email\", Option::<String>::None)?\n .set_override_option(\n \"notifications.notification_email\",\n std::env::var(\"NOTIFICATION_EMAIL\").ok(),\n )?\n // Control API\n .set_default(\"control_api_port\", 8080)?\n .set_override_option(\n \"control_api_port\",\n std::env::var(\"CONTROL_API_PORT\")\n .ok()\n .and_then(|v| v.parse::<i64>().ok()),\n )?\n .build()?;\n\n settings.try_deserialize()\n }\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n\n #[test]\n fn test_default_settings() {\n // This will use defaults since no .env is loaded in tests\n std::env::set_var(\"DATABASE_URL\", \"postgres://test:test@localhost/test\");\n let settings = Settings::from_env();\n assert!(settings.is_ok());\n }\n}\n"}, "8de09e9a6020de5cafc50481a905aa9ccf805852": {"t": 1785757457.8409154, "n": 14217, "h": "e3a1f4e85742a5c4", "c": "//! Application settings loaded from environment variables\n\nuse serde::Deserialize;\n\n/// Root configuration structure\n#[derive(Debug, Clone, Deserialize)]\npub struct Settings {\n pub database: DatabaseConfig,\n pub s3: S3Config,\n pub llm: LlmConfig,\n pub tts: TtsConfig,\n pub assets: AssetConfig,\n pub youtube: YouTubeConfig,\n pub channel: ChannelConfig,\n pub content_strategy: ContentStrategyConfig,\n pub script_improvement: ScriptImprovementConfig,\n pub notifications: NotificationConfig,\n pub control_api_port: u16,\n}\n\n#[derive(Debug, Clone, Deserialize)]\npub struct DatabaseConfig {\n pub url: String,\n}\n\n#[derive(Debug, Clone, Deserialize)]\npub struct S3Config {\n pub endpoint: String,\n pub access_key: String,\n pub secret_key: String,\n pub bucket: String,\n pub region: String,\n}\n\n#[derive(Debug, Clone, Deserialize)]\npub struct LlmConfig {\n pub deepseek_api_key: String,\n pub deepseek_base_url: String,\n pub gemini_api_key: String,\n /// Which provider to use by default: \"deepseek\" or \"gemini\"\n pub default_provider: String,\n}\n\n#[derive(Debug, Clone, Deserialize)]\npub struct TtsConfig {\n /// TTS provider: elevenlabs, qwen3, openai, coqui, piper\n pub provider: String,\n \n // ElevenLabs settings\n pub elevenlabs_api_key: Option<String>,\n pub elevenlabs_voice_id: Option<String>,\n pub elevenlabs_model_id: Option<String>,\n \n // Qwen3-TTS settings (self-hosted, OpenAI-compatible)\n pub qwen3_api_url: Option<String>,\n pub qwen3_api_key: Option<String>,\n pub qwen3_voice: Option<String>,\n \n // OpenAI TTS settings\n pub openai_api_key: Option<String>,\n pub openai_voice: Option<String>,\n pub openai_model: Option<String>,\n \n // Local TTS settings (Coqui/Piper)\n pub local_model_path: Option<String>,\n pub local_speaker_id: Option<String>,\n \n // Common settings\n pub stability: f32,\n pub similarity_boost: f32,\n pub speed: f32,\n}\n\n#[derive(Debug, Clone, Deserialize)]\npub struct YouTubeConfig {\n pub client_id: String,\n pub client_secret: String,\n pub refresh_token: String,\n}\n\n#[derive(Debug, Clone, Deserialize)]\npub struct ChannelConfig {\n pub name: String,\n pub tagline: String,\n}\n\n#[derive(Debug, Clone, Deserialize)]\npub struct ContentStrategyConfig {\n pub target_duration_min: u32,\n pub target_duration_max: u32,\n pub videos_per_week: u32,\n}\n\n#[derive(Debug, Clone, Deserialize)]\npub struct NotificationConfig {\n pub discord_webhook_url: Option<String>,\n pub smtp_host: Option<String>,\n pub smtp_port: Option<u16>,\n pub smtp_user: Option<String>,\n pub smtp_pass: Option<String>,\n pub notification_email: Option<String>,\n}\n\n/// Pexels and Stable Diffusion asset config\n#[derive(Debug, Clone, Deserialize)]\npub struct AssetConfig {\n pub pexels_api_key: String,\n pub sd_api_url: Option<String>,\n pub sd_api_key: Option<String>,\n}\n\n/// Script self-improvement loop configuration\n#[derive(Debug, Clone, Deserialize)]\npub struct ScriptImprovementConfig {\n /// Enable the self-improvement loop\n pub enabled: bool,\n /// Number of initial script candidates to generate in parallel\n pub candidate_count: u32,\n /// Minimum score (1.0-10.0) to accept without refinement\n pub quality_threshold: f32,\n /// Maximum refinement iterations before accepting best available\n pub max_iterations: u32,\n /// Total timeout for the entire self-improvement process (seconds)\n pub timeout_seconds: u32,\n}\n\nimpl Settings {\n /// Load settings from environment variables\n pub fn from_env() -> Result<Self, config::ConfigError> {\n dotenvy::dotenv().ok();\n\n let settings = config::Config::builder()\n // Database\n .set_default(\"database.url\", \"\")?\n .set_override_option(\n \"database.url\",\n std::env::var(\"DATABASE_URL\").ok(),\n )?\n // S3\n .set_default(\"s3.endpoint\", \"http://localhost:9000\")?\n .set_override_option(\"s3.endpoint\", std::env::var(\"S3_ENDPOINT\").ok())?\n .set_default(\"s3.access_key\", \"minioadmin\")?\n .set_override_option(\"s3.access_key\", std::env::var(\"S3_ACCESS_KEY\").ok())?\n .set_default(\"s3.secret_key\", \"minioadmin\")?\n .set_override_option(\"s3.secret_key\", std::env::var(\"S3_SECRET_KEY\").ok())?\n .set_default(\"s3.bucket\", \"animus-assets\")?\n .set_override_option(\"s3.bucket\", std::env::var(\"S3_BUCKET\").ok())?\n .set_default(\"s3.region\", \"us-east-1\")?\n .set_override_option(\"s3.region\", std::env::var(\"S3_REGION\").ok())?\n // LLM\n .set_default(\"llm.deepseek_api_key\", \"\")?\n .set_override_option(\n \"llm.deepseek_api_key\",\n std::env::var(\"DEEPSEEK_API_KEY\").ok(),\n )?\n .set_default(\"llm.deepseek_base_url\", \"https://api.deepseek.com\")?\n .set_override_option(\n \"llm.deepseek_base_url\",\n std::env::var(\"DEEPSEEK_BASE_URL\").ok(),\n )?\n .set_default(\"llm.gemini_api_key\", \"\")?\n .set_override_option(\"llm.gemini_api_key\", std::env::var(\"GEMINI_API_KEY\").ok())?\n .set_default(\"llm.default_provider\", \"deepseek\")?\n .set_override_option(\n \"llm.default_provider\",\n std::env::var(\"LLM_DEFAULT_PROVIDER\").ok(),\n )?\n // TTS\n .set_default(\"tts.provider\", \"elevenlabs\")?\n .set_override_option(\"tts.provider\", std::env::var(\"TTS_PROVIDER\").ok())?\n // ElevenLabs\n .set_default(\"tts.elevenlabs_api_key\", Option::<String>::None)?\n .set_override_option(\n \"tts.elevenlabs_api_key\",\n std::env::var(\"ELEVENLABS_API_KEY\").ok(),\n )?\n .set_default(\"tts.elevenlabs_voice_id\", Option::<String>::None)?\n .set_override_option(\n \"tts.elevenlabs_voice_id\",\n std::env::var(\"ELEVENLABS_VOICE_ID\").ok(),\n )?\n .set_default(\"tts.elevenlabs_model_id\", Option::<String>::None)?\n .set_override_option(\n \"tts.elevenlabs_model_id\",\n std::env::var(\"ELEVENLABS_MODEL_ID\").ok(),\n )?\n // Qwen3-TTS\n .set_default(\"tts.qwen3_api_url\", Option::<String>::None)?\n .set_override_option(\"tts.qwen3_api_url\", std::env::var(\"QWEN3_API_URL\").ok())?\n .set_default(\"tts.qwen3_api_key\", Option::<String>::None)?\n .set_override_option(\"tts.qwen3_api_key\", std::env::var(\"QWEN3_API_KEY\").ok())?\n .set_default(\"tts.qwen3_voice\", Option::<String>::None)?\n .set_override_option(\"tts.qwen3_voice\", std::env::var(\"QWEN3_VOICE\").ok())?\n // OpenAI TTS\n .set_default(\"tts.openai_api_key\", Option::<String>::None)?\n .set_override_option(\"tts.openai_api_key\", std::env::var(\"OPENAI_TTS_API_KEY\").ok())?\n .set_default(\"tts.openai_voice\", Option::<String>::None)?\n .set_override_option(\"tts.openai_voice\", std::env::var(\"OPENAI_TTS_VOICE\").ok())?\n .set_default(\"tts.openai_model\", Option::<String>::None)?\n .set_override_option(\"tts.openai_model\", std::env::var(\"OPENAI_TTS_MODEL\").ok())?\n // Local TTS\n .set_default(\"tts.local_model_path\", Option::<String>::None)?\n .set_override_option(\"tts.local_model_path\", std::env::var(\"LOCAL_TTS_MODEL_PATH\").ok())?\n .set_default(\"tts.local_speaker_id\", Option::<String>::None)?\n .set_override_option(\"tts.local_speaker_id\", std::env::var(\"LOCAL_TTS_SPEAKER_ID\").ok())?\n // Common TTS settings\n .set_default(\"tts.stability\", 0.5)?\n .set_default(\"tts.similarity_boost\", 0.75)?\n .set_default(\"tts.speed\", 1.0)?\n .set_override_option(\n \"tts.speed\",\n std::env::var(\"TTS_SPEED\").ok().and_then(|v| v.parse::<f64>().ok()),\n )?\n // Assets (Pexels, Stable Diffusion)\n .set_default(\"assets.pexels_api_key\", \"\")?\n .set_override_option(\n \"assets.pexels_api_key\",\n std::env::var(\"PEXELS_API_KEY\").ok(),\n )?\n .set_default(\"assets.sd_api_url\", Option::<String>::None)?\n .set_override_option(\"assets.sd_api_url\", std::env::var(\"SD_API_URL\").ok())?\n .set_default(\"assets.sd_api_key\", Option::<String>::None)?\n .set_override_option(\"assets.sd_api_key\", std::env::var(\"SD_API_KEY\").ok())?\n // YouTube\n .set_default(\"youtube.client_id\", \"\")?\n .set_override_option(\n \"youtube.client_id\",\n std::env::var(\"YOUTUBE_CLIENT_ID\").ok(),\n )?\n .set_default(\"youtube.client_secret\", \"\")?\n .set_override_option(\n \"youtube.client_secret\",\n std::env::var(\"YOUTUBE_CLIENT_SECRET\").ok(),\n )?\n .set_default(\"youtube.refresh_token\", \"\")?\n .set_override_option(\n \"youtube.refresh_token\",\n std::env::var(\"YOUTUBE_REFRESH_TOKEN\").ok(),\n )?\n // Channel\n .set_default(\"channel.name\", \"Excelsior Academy\")?\n .set_override_option(\"channel.name\", std::env::var(\"CHANNEL_NAME\").ok())?\n .set_default(\"channel.tagline\", \"Wisdom for the journey upward\")?\n .set_override_option(\"channel.tagline\", std::env::var(\"CHANNEL_TAGLINE\").ok())?\n // Content Strategy\n .set_default(\"content_strategy.target_duration_min\", 12)?\n .set_override_option(\n \"content_strategy.target_duration_min\",\n std::env::var(\"TARGET_DURATION_MIN\")\n .ok()\n .and_then(|v| v.parse::<i64>().ok()),\n )?\n .set_default(\"content_strategy.target_duration_max\", 20)?\n .set_override_option(\n \"content_strategy.target_duration_max\",\n std::env::var(\"TARGET_DURATION_MAX\")\n .ok()\n .and_then(|v| v.parse::<i64>().ok()),\n )?\n .set_default(\"content_strategy.videos_per_week\", 4)?\n .set_override_option(\n \"content_strategy.videos_per_week\",\n std::env::var(\"VIDEOS_PER_WEEK\")\n .ok()\n .and_then(|v| v.parse::<i64>().ok()),\n )?\n // Script Self-Improvement Loop\n .set_default(\"script_improvement.enabled\", true)?\n .set_override_option(\n \"script_improvement.enabled\",\n std::env::var(\"SCRIPT_IMPROVEMENT_ENABLED\")\n .ok()\n .map(|v| v.to_lowercase() == \"true\" || v == \"1\"),\n )?\n .set_default(\"script_improvement.candidate_count\", 3)?\n .set_override_option(\n \"script_improvement.candidate_count\",\n std::env::var(\"SCRIPT_IMPROVEMENT_CANDIDATES\")\n .ok()\n .and_then(|v| v.parse::<i64>().ok()),\n )?\n .set_default(\"script_improvement.quality_threshold\", 8.0)?\n .set_override_option(\n \"script_improvement.quality_threshold\",\n std::env::var(\"SCRIPT_IMPROVEMENT_THRESHOLD\")\n .ok()\n .and_then(|v| v.parse::<f64>().ok()),\n )?\n .set_default(\"script_improvement.max_iterations\", 10)?\n .set_override_option(\n \"script_improvement.max_iterations\",\n std::env::var(\"SCRIPT_IMPROVEMENT_MAX_ITERATIONS\")\n .ok()\n .and_then(|v| v.parse::<i64>().ok()),\n )?\n .set_default(\"script_improvement.timeout_seconds\", 1800)?\n .set_override_option(\n \"script_improvement.timeout_seconds\",\n std::env::var(\"SCRIPT_IMPROVEMENT_TIMEOUT\")\n .ok()\n .and_then(|v| v.parse::<i64>().ok()),\n )?\n // Notifications\n .set_default(\"notifications.discord_webhook_url\", Option::<String>::None)?\n .set_override_option(\n \"notifications.discord_webhook_url\",\n std::env::var(\"DISCORD_WEBHOOK_URL\").ok(),\n )?\n .set_default(\"notifications.smtp_host\", Option::<String>::None)?\n .set_override_option(\"notifications.smtp_host\", std::env::var(\"SMTP_HOST\").ok())?\n .set_default(\"notifications.smtp_port\", Option::<u16>::None)?\n .set_override_option(\n \"notifications.smtp_port\",\n std::env::var(\"SMTP_PORT\")\n .ok()\n .and_then(|v| v.parse::<i64>().ok()),\n )?\n .set_default(\"notifications.smtp_user\", Option::<String>::None)?\n .set_override_option(\"notifications.smtp_user\", std::env::var(\"SMTP_USER\").ok())?\n .set_default(\"notifications.smtp_pass\", Option::<String>::None)?\n .set_override_option(\"notifications.smtp_pass\", std::env::var(\"SMTP_PASS\").ok())?\n .set_default(\"notifications.notification_email\", Option::<String>::None)?\n .set_override_option(\n \"notifications.notification_email\",\n std::env::var(\"NOTIFICATION_EMAIL\").ok(),\n )?\n // Control API\n .set_default(\"control_api_port\", 8080)?\n .set_override_option(\n \"control_api_port\",\n std::env::var(\"CONTROL_API_PORT\")\n .ok()\n .and_then(|v| v.parse::<i64>().ok()),\n )?\n .build()?;\n\n settings.try_deserialize()\n }\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n\n #[test]\n fn test_default_settings() {\n // This will use defaults since no .env is loaded in tests\n std::env::set_var(\"DATABASE_URL\", \"postgres://test:test@localhost/test\");\n let settings = Settings::from_env();\n assert!(settings.is_ok());\n }\n}\n"}, "f09f434ea7529bb1e6e09f308ecce5900035ed4c": {"t": 1785757459.2126722, "n": 11950, "h": "f35d8dd5b9bcd69d", "c": "//! Application settings loaded from environment variables\n\nuse serde::Deserialize;\n\n/// Root configuration structure\n#[derive(Debug, Clone, Deserialize)]\npub struct Settings {\n pub database: DatabaseConfig,\n pub s3: S3Config,\n pub llm: LlmConfig,\n pub tts: TtsConfig,\n pub assets: AssetConfig,\n pub youtube: YouTubeConfig,\n pub channel: ChannelConfig,\n pub content_strategy: ContentStrategyConfig,\n pub notifications: NotificationConfig,\n pub control_api_port: u16,\n}\n\n#[derive(Debug, Clone, Deserialize)]\npub struct DatabaseConfig {\n pub url: String,\n}\n\n#[derive(Debug, Clone, Deserialize)]\npub struct S3Config {\n pub endpoint: String,\n pub access_key: String,\n pub secret_key: String,\n pub bucket: String,\n pub region: String,\n}\n\n#[derive(Debug, Clone, Deserialize)]\npub struct LlmConfig {\n pub deepseek_api_key: String,\n pub deepseek_base_url: String,\n pub gemini_api_key: String,\n /// Which provider to use by default: \"deepseek\" or \"gemini\"\n pub default_provider: String,\n}\n\n#[derive(Debug, Clone, Deserialize)]\npub struct TtsConfig {\n /// TTS provider: elevenlabs, qwen3, openai, coqui, piper\n pub provider: String,\n \n // ElevenLabs settings\n pub elevenlabs_api_key: Option<String>,\n pub elevenlabs_voice_id: Option<String>,\n pub elevenlabs_model_id: Option<String>,\n \n // Qwen3-TTS settings (self-hosted, OpenAI-compatible)\n pub qwen3_api_url: Option<String>,\n pub qwen3_api_key: Option<String>,\n pub qwen3_voice: Option<String>,\n \n // OpenAI TTS settings\n pub openai_api_key: Option<String>,\n pub openai_voice: Option<String>,\n pub openai_model: Option<String>,\n \n // Local TTS settings (Coqui/Piper)\n pub local_model_path: Option<String>,\n pub local_speaker_id: Option<String>,\n \n // Common settings\n pub stability: f32,\n pub similarity_boost: f32,\n pub speed: f32,\n}\n\n#[derive(Debug, Clone, Deserialize)]\npub struct YouTubeConfig {\n pub client_id: String,\n pub client_secret: String,\n pub refresh_token: String,\n}\n\n#[derive(Debug, Clone, Deserialize)]\npub struct ChannelConfig {\n pub name: String,\n pub tagline: String,\n}\n\n#[derive(Debug, Clone, Deserialize)]\npub struct ContentStrategyConfig {\n pub target_duration_min: u32,\n pub target_duration_max: u32,\n pub videos_per_week: u32,\n}\n\n#[derive(Debug, Clone, Deserialize)]\npub struct NotificationConfig {\n pub discord_webhook_url: Option<String>,\n pub smtp_host: Option<String>,\n pub smtp_port: Option<u16>,\n pub smtp_user: Option<String>,\n pub smtp_pass: Option<String>,\n pub notification_email: Option<String>,\n}\n\n/// Pexels and Stable Diffusion asset config\n#[derive(Debug, Clone, Deserialize)]\npub struct AssetConfig {\n pub pexels_api_key: String,\n pub sd_api_url: Option<String>,\n pub sd_api_key: Option<String>,\n}\n\nimpl Settings {\n /// Load settings from environment variables\n pub fn from_env() -> Result<Self, config::ConfigError> {\n dotenvy::dotenv().ok();\n\n let settings = config::Config::builder()\n // Database\n .set_default(\"database.url\", \"\")?\n .set_override_option(\n \"database.url\",\n std::env::var(\"DATABASE_URL\").ok(),\n )?\n // S3\n .set_default(\"s3.endpoint\", \"http://localhost:9000\")?\n .set_override_option(\"s3.endpoint\", std::env::var(\"S3_ENDPOINT\").ok())?\n .set_default(\"s3.access_key\", \"minioadmin\")?\n .set_override_option(\"s3.access_key\", std::env::var(\"S3_ACCESS_KEY\").ok())?\n .set_default(\"s3.secret_key\", \"minioadmin\")?\n .set_override_option(\"s3.secret_key\", std::env::var(\"S3_SECRET_KEY\").ok())?\n .set_default(\"s3.bucket\", \"animus-assets\")?\n .set_override_option(\"s3.bucket\", std::env::var(\"S3_BUCKET\").ok())?\n .set_default(\"s3.region\", \"us-east-1\")?\n .set_override_option(\"s3.region\", std::env::var(\"S3_REGION\").ok())?\n // LLM\n .set_default(\"llm.deepseek_api_key\", \"\")?\n .set_override_option(\n \"llm.deepseek_api_key\",\n std::env::var(\"DEEPSEEK_API_KEY\").ok(),\n )?\n .set_default(\"llm.deepseek_base_url\", \"https://api.deepseek.com\")?\n .set_override_option(\n \"llm.deepseek_base_url\",\n std::env::var(\"DEEPSEEK_BASE_URL\").ok(),\n )?\n .set_default(\"llm.gemini_api_key\", \"\")?\n .set_override_option(\"llm.gemini_api_key\", std::env::var(\"GEMINI_API_KEY\").ok())?\n .set_default(\"llm.default_provider\", \"deepseek\")?\n .set_override_option(\n \"llm.default_provider\",\n std::env::var(\"LLM_DEFAULT_PROVIDER\").ok(),\n )?\n // TTS\n .set_default(\"tts.provider\", \"elevenlabs\")?\n .set_override_option(\"tts.provider\", std::env::var(\"TTS_PROVIDER\").ok())?\n // ElevenLabs\n .set_default(\"tts.elevenlabs_api_key\", Option::<String>::None)?\n .set_override_option(\n \"tts.elevenlabs_api_key\",\n std::env::var(\"ELEVENLABS_API_KEY\").ok(),\n )?\n .set_default(\"tts.elevenlabs_voice_id\", Option::<String>::None)?\n .set_override_option(\n \"tts.elevenlabs_voice_id\",\n std::env::var(\"ELEVENLABS_VOICE_ID\").ok(),\n )?\n .set_default(\"tts.elevenlabs_model_id\", Option::<String>::None)?\n .set_override_option(\n \"tts.elevenlabs_model_id\",\n std::env::var(\"ELEVENLABS_MODEL_ID\").ok(),\n )?\n // Qwen3-TTS\n .set_default(\"tts.qwen3_api_url\", Option::<String>::None)?\n .set_override_option(\"tts.qwen3_api_url\", std::env::var(\"QWEN3_API_URL\").ok())?\n .set_default(\"tts.qwen3_api_key\", Option::<String>::None)?\n .set_override_option(\"tts.qwen3_api_key\", std::env::var(\"QWEN3_API_KEY\").ok())?\n .set_default(\"tts.qwen3_voice\", Option::<String>::None)?\n .set_override_option(\"tts.qwen3_voice\", std::env::var(\"QWEN3_VOICE\").ok())?\n // OpenAI TTS\n .set_default(\"tts.openai_api_key\", Option::<String>::None)?\n .set_override_option(\"tts.openai_api_key\", std::env::var(\"OPENAI_TTS_API_KEY\").ok())?\n .set_default(\"tts.openai_voice\", Option::<String>::None)?\n .set_override_option(\"tts.openai_voice\", std::env::var(\"OPENAI_TTS_VOICE\").ok())?\n .set_default(\"tts.openai_model\", Option::<String>::None)?\n .set_override_option(\"tts.openai_model\", std::env::var(\"OPENAI_TTS_MODEL\").ok())?\n // Local TTS\n .set_default(\"tts.local_model_path\", Option::<String>::None)?\n .set_override_option(\"tts.local_model_path\", std::env::var(\"LOCAL_TTS_MODEL_PATH\").ok())?\n .set_default(\"tts.local_speaker_id\", Option::<String>::None)?\n .set_override_option(\"tts.local_speaker_id\", std::env::var(\"LOCAL_TTS_SPEAKER_ID\").ok())?\n // Common TTS settings\n .set_default(\"tts.stability\", 0.5)?\n .set_default(\"tts.similarity_boost\", 0.75)?\n .set_default(\"tts.speed\", 1.0)?\n .set_override_option(\n \"tts.speed\",\n std::env::var(\"TTS_SPEED\").ok().and_then(|v| v.parse::<f64>().ok()),\n )?\n // Assets (Pexels, Stable Diffusion)\n .set_default(\"assets.pexels_api_key\", \"\")?\n .set_override_option(\n \"assets.pexels_api_key\",\n std::env::var(\"PEXELS_API_KEY\").ok(),\n )?\n .set_default(\"assets.sd_api_url\", Option::<String>::None)?\n .set_override_option(\"assets.sd_api_url\", std::env::var(\"SD_API_URL\").ok())?\n .set_default(\"assets.sd_api_key\", Option::<String>::None)?\n .set_override_option(\"assets.sd_api_key\", std::env::var(\"SD_API_KEY\").ok())?\n // YouTube\n .set_default(\"youtube.client_id\", \"\")?\n .set_override_option(\n \"youtube.client_id\",\n std::env::var(\"YOUTUBE_CLIENT_ID\").ok(),\n )?\n .set_default(\"youtube.client_secret\", \"\")?\n .set_override_option(\n \"youtube.client_secret\",\n std::env::var(\"YOUTUBE_CLIENT_SECRET\").ok(),\n )?\n .set_default(\"youtube.refresh_token\", \"\")?\n .set_override_option(\n \"youtube.refresh_token\",\n std::env::var(\"YOUTUBE_REFRESH_TOKEN\").ok(),\n )?\n // Channel\n .set_default(\"channel.name\", \"Excelsior Academy\")?\n .set_override_option(\"channel.name\", std::env::var(\"CHANNEL_NAME\").ok())?\n .set_default(\"channel.tagline\", \"Wisdom for the journey upward\")?\n .set_override_option(\"channel.tagline\", std::env::var(\"CHANNEL_TAGLINE\").ok())?\n // Content Strategy\n .set_default(\"content_strategy.target_duration_min\", 12)?\n .set_override_option(\n \"content_strategy.target_duration_min\",\n std::env::var(\"TARGET_DURATION_MIN\")\n .ok()\n .and_then(|v| v.parse::<i64>().ok()),\n )?\n .set_default(\"content_strategy.target_duration_max\", 20)?\n .set_override_option(\n \"content_strategy.target_duration_max\",\n std::env::var(\"TARGET_DURATION_MAX\")\n .ok()\n .and_then(|v| v.parse::<i64>().ok()),\n )?\n .set_default(\"content_strategy.videos_per_week\", 4)?\n .set_override_option(\n \"content_strategy.videos_per_week\",\n std::env::var(\"VIDEOS_PER_WEEK\")\n .ok()\n .and_then(|v| v.parse::<i64>().ok()),\n )?\n // Notifications\n .set_default(\"notifications.discord_webhook_url\", Option::<String>::None)?\n .set_override_option(\n \"notifications.discord_webhook_url\",\n std::env::var(\"DISCORD_WEBHOOK_URL\").ok(),\n )?\n .set_default(\"notifications.smtp_host\", Option::<String>::None)?\n .set_override_option(\"notifications.smtp_host\", std::env::var(\"SMTP_HOST\").ok())?\n .set_default(\"notifications.smtp_port\", Option::<u16>::None)?\n .set_override_option(\n \"notifications.smtp_port\",\n std::env::var(\"SMTP_PORT\")\n .ok()\n .and_then(|v| v.parse::<i64>().ok()),\n )?\n .set_default(\"notifications.smtp_user\", Option::<String>::None)?\n .set_override_option(\"notifications.smtp_user\", std::env::var(\"SMTP_USER\").ok())?\n .set_default(\"notifications.smtp_pass\", Option::<String>::None)?\n .set_override_option(\"notifications.smtp_pass\", std::env::var(\"SMTP_PASS\").ok())?\n .set_default(\"notifications.notification_email\", Option::<String>::None)?\n .set_override_option(\n \"notifications.notification_email\",\n std::env::var(\"NOTIFICATION_EMAIL\").ok(),\n )?\n // Control API\n .set_default(\"control_api_port\", 8080)?\n .set_override_option(\n \"control_api_port\",\n std::env::var(\"CONTROL_API_PORT\")\n .ok()\n .and_then(|v| v.parse::<i64>().ok()),\n )?\n .build()?;\n\n settings.try_deserialize()\n }\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n\n #[test]\n fn test_default_settings() {\n // This will use defaults since no .env is loaded in tests\n std::env::set_var(\"DATABASE_URL\", \"postgres://test:test@localhost/test\");\n let settings = Settings::from_env();\n assert!(settings.is_ok());\n }\n}\n"}}}}