jebin2 commited on
Commit
a722bbb
Β·
1 Parent(s): de09351

refactor: simplify config to .env only, remove TOML

Browse files

- Remove all config.toml files from setup/ folders
- Simplify config.py: remove TOML parsing, flatten logic
- All config now from environment variables only
- Keep setup/ folders for documentation purposes
- Remove unused functions (get_gcp_project_id, list_available_setups)

setup/beats_cut/config.toml DELETED
@@ -1,16 +0,0 @@
1
- # Beats Cut Random Videos Setup
2
- # Uses beat detection to sync video cuts with music
3
-
4
- [general]
5
- setup_type = "beats_cut"
6
- description = "Random videos with cuts synced to music beats"
7
-
8
- [video]
9
- beat_method = "downbeat"
10
-
11
- [gsheet]
12
- name = "Infloxa Data for Elvoro"
13
- id = "1djnE1u_QCveGlhjNnZRfXiY-3NLzO0V-04tZ9P2mVcs"
14
- video_library_worksheet = "Video Library"
15
- audio_library_worksheet = "Audio Library"
16
- logs_worksheet = "Infloxa Data for Elvoro LOGS"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
setup/hard_cut/config.toml DELETED
@@ -1,16 +0,0 @@
1
- # Hard Cut Random Videos Setup
2
- # Uses fixed intervals instead of beat detection for video cuts
3
-
4
- [general]
5
- setup_type = "hard_cut"
6
- description = "Random videos with hard cuts at fixed intervals"
7
-
8
- [video]
9
- hard_cut_random_videos_interval = "0.5"
10
-
11
- [gsheet]
12
- name = "Infloxa Data for Elvoro"
13
- id = "1djnE1u_QCveGlhjNnZRfXiY-3NLzO0V-04tZ9P2mVcs"
14
- video_library_worksheet = "Video Library"
15
- audio_library_worksheet = "Audio Library"
16
- logs_worksheet = "Infloxa Data for Elvoro LOGS"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/config.py CHANGED
@@ -1,12 +1,11 @@
1
  """
2
  Unified Configuration Module
3
 
4
- Combines functionality from load_config.py and setup_config.py to provide
5
- a single source of truth for application configuration.
6
  """
7
 
8
  import os
9
- import sys
10
  import json
11
  import logging
12
  from pathlib import Path
@@ -19,165 +18,11 @@ from google.auth import default
19
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
20
  logger = logging.getLogger(__name__)
21
 
22
- # Use tomllib (Python 3.11+) or fall back to tomli
23
- if sys.version_info >= (3, 11):
24
- import tomllib
25
- else:
26
- try:
27
- import tomli as tomllib
28
- except ImportError:
29
- tomllib = None
30
-
31
  # ------------------ Singleton & Cache ------------------
32
 
33
  _cached_config: Optional[Dict[str, Any]] = None
34
- _cached_setup_name: Optional[str] = None
35
  _config_initialized: bool = False
36
 
37
- # ------------------ Setup Config Logic ------------------
38
-
39
- def get_setup_dir() -> Path:
40
- """Get the setup directory path."""
41
- # Go from src/ to project root, then into setup/
42
- project_root = Path(__file__).parent.parent
43
- return project_root / "setup"
44
-
45
-
46
- def list_available_setups() -> list[str]:
47
- """List all available setup configurations."""
48
- setup_dir = get_setup_dir()
49
- setups = []
50
-
51
- if setup_dir.exists():
52
- for item in setup_dir.iterdir():
53
- if item.is_dir() and (item / "config.toml").exists():
54
- setups.append(item.name)
55
-
56
- return sorted(setups)
57
-
58
-
59
- def _flatten_config(config: Dict[str, Any], prefix: str = "") -> Dict[str, Any]:
60
- """
61
- Flatten nested TOML config into a flat dictionary.
62
- """
63
- result = {}
64
-
65
- for key, value in config.items():
66
- full_key = f"{prefix}.{key}" if prefix else key
67
-
68
- if isinstance(value, dict):
69
- # Recurse into nested dicts
70
- nested = _flatten_config(value, full_key)
71
- result.update(nested)
72
- else:
73
- # Add both the full nested key and the simple key
74
- result[full_key] = value
75
- # Also add simple key if it doesn't conflict
76
- if key not in result:
77
- result[key] = value
78
-
79
- return result
80
-
81
-
82
- def _apply_env_overrides(config: Dict[str, Any]) -> Dict[str, Any]:
83
- """
84
- Apply environment variable overrides to config values.
85
- """
86
- result = config.copy()
87
-
88
- # Map of config keys to env var names
89
- env_mappings = {
90
- "only_random_videos": "ONLY_RANDOM_VIDEOS",
91
- "hard_cut_random_videos": "HARD_CUT_RANDOM_VIDEOS",
92
- "use_veo": "USE_VEO",
93
- "beat_method": "BEAT_METHOD",
94
- "setup_type": "SETUP_TYPE",
95
- "caption_style": "CAPTION_STYLE",
96
- "content_strategy_file": "CONTENT_STRATEGY_FILE",
97
- "on_screen_text": "ON_SCREEN_TEXT",
98
- "is_onscreen_cta": "IS_ONSCREEN_CTA",
99
- "is_a2e_lip_sync": "IS_A2E_LIP_SYNC",
100
- "use_1x1_ratio": "USE_1X1_RATIO",
101
- # GSheet mappings
102
- "gsheet.name": "GSHEET_NAME",
103
- "gsheet.id": "GSHEET_ID",
104
- "video_library_worksheet": "VIDEO_LIBRARY_GSHEET_WORKSHEET",
105
- "audio_library_worksheet": "AUDIO_LIBRARY_GSHEET_WORKSHEET",
106
- "logs_worksheet": "GSHEET_WORKSHEET_LOGS",
107
- "content_strategy_worksheet": "CONTENT_STRATEGY_GSHEET_WORKSHEET",
108
- "test_automation": "TEST_AUTOMATION",
109
- "test_data_directory": "TEST_DATA_DIRECTORY",
110
- "delete_all_a2e_videos": "DELETE_ALL_A2E_VIDEOS",
111
- "tiktok_access_token": "TIKTOK_ACCESS_TOKEN",
112
- "tiktok_privacy_level": "TIKTOK_PRIVACY_LEVEL",
113
- "youtube_refresh_token": "YOUTUBE_REFRESH_TOKEN",
114
- "youtube_client_secrets_json": "YOUTUBE_CLIENT_SECRETS_JSON",
115
- "youtube_client_secrets_base64": "YOUTUBE_CLIENT_SECRETS_BASE64",
116
- "youtube_client_id": "YOUTUBE_CLIENT_ID",
117
- "youtube_client_secret": "YOUTUBE_CLIENT_SECRET",
118
- "video_category": "VIDEO_CATEGORY",
119
- "video_privacy": "VIDEO_PRIVACY",
120
- "scheduled_time": "SCHEDULED_TIME",
121
- "instagram_access_token": "INSTAGRAM_ACCESS_TOKEN",
122
- "instagram_account_id": "INSTAGRAM_ACCOUNT_ID",
123
- "commit_progress": "COMMIT_PROGRESS",
124
- "use_gemimi_video": "USE_GEMIMI_VIDEO",
125
- "plain_video_count": "PlAIN_VIDEO_COUNT",
126
- }
127
-
128
- for config_key, env_var in env_mappings.items():
129
- env_value = os.getenv(env_var)
130
- if env_value is not None:
131
- # Convert to appropriate type based on existing config value
132
- if config_key in result:
133
- original_type = type(result[config_key])
134
- if original_type == bool:
135
- result[config_key] = env_value.lower() in ("true", "1", "yes")
136
- elif original_type == int:
137
- try:
138
- result[config_key] = int(env_value)
139
- except ValueError:
140
- pass
141
- else:
142
- result[config_key] = env_value
143
- else:
144
- result[config_key] = env_value
145
-
146
- return result
147
-
148
-
149
- def _load_setup_config_data(setup_name: Optional[str] = None) -> Dict[str, Any]:
150
- """Helper to load and process the TOML configuration file."""
151
- if setup_name is None:
152
- setup_name = os.getenv("SETUP_NAME")
153
-
154
- if not setup_name:
155
- # If no setup name is provided or in env, return empty or raise?
156
- # Preserving logic from setup_config.py which raised an error only if strictly required
157
- # But here we want to handle the case where it might be missing gracefully if usage allows
158
- return {}
159
-
160
- setup_dir = get_setup_dir()
161
- config_path = setup_dir / setup_name / "config.toml"
162
-
163
- if not config_path.exists():
164
- # Fallback or error handled by caller/validator
165
- return {}
166
-
167
- if tomllib is None:
168
- logger.warning("TOML supported needed but not available. Install 'tomli' for Python < 3.11")
169
- return {}
170
-
171
- try:
172
- with open(config_path, "rb") as f:
173
- raw_config = tomllib.load(f)
174
-
175
- flat_config = _flatten_config(raw_config)
176
- return _apply_env_overrides(flat_config)
177
- except Exception as e:
178
- logger.error(f"Error loading setup config: {e}")
179
- return {}
180
-
181
 
182
  # ------------------ GCP Auth Configuration ------------------
183
 
@@ -195,7 +40,6 @@ def _resolve_gcp_project_id() -> tuple[Optional[str], Optional[str]]:
195
  os.getenv("GOOGLE_APPLICATION_CREDENTIALS"))
196
 
197
  if gcp_creds_path:
198
- # Set temp bucket env var side-effect (from original load_config)
199
  os.environ["MY_TEMP_GCS_BUCKET"] = os.getenv("MY_TEMP_GCS_BUCKET", "")
200
 
201
  try:
@@ -218,7 +62,6 @@ def _resolve_gcp_project_id() -> tuple[Optional[str], Optional[str]]:
218
  # 2. Workload Identity Federation / ADC
219
  if not gcp_project_id:
220
  try:
221
- # This handles both WIF and ADC
222
  creds, project = default()
223
  if project:
224
  gcp_project_id = project
@@ -263,58 +106,92 @@ def _resolve_gcp_project_id() -> tuple[Optional[str], Optional[str]]:
263
  return gcp_project_id, auth_method
264
 
265
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
266
  # ------------------ Main Load Function ------------------
267
 
268
  def load_configuration(force_reload: bool = False) -> Dict[str, Any]:
269
  """
270
- Load configuration from all sources.
271
 
272
  1. Load .env
273
- 2. Load Setup Config (TOML)
274
- 3. Resolve GCP Project & Secrets
275
- 4. Merge & Validate
276
  """
277
- global _cached_config, _cached_setup_name, _config_initialized
278
 
279
- setup_name = os.getenv("SETUP_NAME")
280
-
281
  # Return cache if valid
282
- if (_config_initialized and not force_reload and
283
- _cached_config is not None and
284
- _cached_setup_name == setup_name):
285
  return _cached_config
286
 
287
  load_dotenv()
288
 
289
- # Load Setup Config (TOML)
290
- # Note: We don't fail hard here if SETUP_NAME is missing, we just get empty setup config
291
- # Validation happens later if critical keys are missing.
292
- setup_config = _load_setup_config_data(setup_name)
293
- if setup_config:
294
- logger.info(f"βœ“ Loaded setup config: {setup_name}")
295
 
296
  # Resolve GCP Project
297
  gcp_project_id, auth_method = _resolve_gcp_project_id()
298
  if gcp_project_id:
299
  logger.info(f"βœ“ GCP Project ID: {gcp_project_id} ({auth_method})")
300
 
301
- # Merge into final config
302
  config = {
303
- **setup_config,
304
- "gemini_api_key": os.getenv("GEMINI_API_KEY"),
305
- "runwayml_api_key": os.getenv("RUNWAYML_API_KEY"),
306
- "gcs_bucket_name": os.getenv("GCS_BUCKET_NAME"),
307
  "gcp_project_id": gcp_project_id,
308
  "auth_method": auth_method,
309
- "setup_name": setup_name,
310
- "setup_name": setup_name,
 
 
311
  "a2e_api_key": os.getenv("A2E_API_KEY"),
312
- "test_automation": os.getenv("TEST_AUTOMATION", "false").lower() == "true",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
313
  "test_data_directory": os.getenv("TEST_DATA_DIRECTORY"),
314
- "delete_all_a2e_videos": os.getenv("DELETE_ALL_A2E_VIDEOS", "false").lower() == "true",
 
 
315
  "tiktok_access_token": os.getenv("TIKTOK_ACCESS_TOKEN"),
316
  "tiktok_privacy_level": os.getenv("TIKTOK_PRIVACY_LEVEL", "PUBLIC_TO_EVERYONE"),
317
- "content_strategy_worksheet": os.getenv("CONTENT_STRATEGY_GSHEET_WORKSHEET"),
 
318
  "youtube_refresh_token": os.getenv("YOUTUBE_REFRESH_TOKEN"),
319
  "youtube_client_secrets_json": os.getenv("YOUTUBE_CLIENT_SECRETS_JSON"),
320
  "youtube_client_secrets_base64": os.getenv("YOUTUBE_CLIENT_SECRETS_BASE64"),
@@ -323,13 +200,16 @@ def load_configuration(force_reload: bool = False) -> Dict[str, Any]:
323
  "video_category": os.getenv("VIDEO_CATEGORY", "22"),
324
  "video_privacy": os.getenv("VIDEO_PRIVACY", "private"),
325
  "scheduled_time": os.getenv("SCHEDULED_TIME"),
 
 
326
  "instagram_access_token": os.getenv("INSTAGRAM_ACCESS_TOKEN"),
327
  "instagram_account_id": os.getenv("INSTAGRAM_ACCOUNT_ID"),
328
- "commit_progress": os.getenv("COMMIT_PROGRESS", "false").lower() == "true",
329
- "use_gemimi_video": os.getenv("USE_GEMIMI_VIDEO", "false").lower() == "true",
330
- "plain_video_count": int(os.getenv("PlAIN_VIDEO_COUNT", 100)),
331
  }
332
 
 
333
  config["on_screen_cta"] = [
334
  "LINK IN BIO πŸ›οΈ",
335
  "πŸ”— LINK IN ACCOUNT πŸ›οΈ",
@@ -339,15 +219,12 @@ def load_configuration(force_reload: bool = False) -> Dict[str, Any]:
339
  "πŸ›οΈ SALE ENDING SOON - πŸ”— LINK IN BIO"
340
  ]
341
 
342
- # Initialize default empty collections for usage tracking
343
  config["video_usage_count"] = {}
344
  config["avatar_usage_count"] = {}
345
  config["visual_assets"] = {}
346
 
347
- # Required keys validation (soft validation - log error but don't crash module import)
348
- # Crashes should handle at application start
349
  _cached_config = config
350
- _cached_setup_name = setup_name
351
  _config_initialized = True
352
 
353
  return config
@@ -400,9 +277,7 @@ config = ConfigProxy()
400
 
401
 
402
  def get_config_value(key: str, default: Any = None) -> Any:
403
- """
404
- Get a config value. Preserves all types (dict, list, str, int, etc.).
405
- """
406
  value = config.get(key, default)
407
  return value if value is not None else default
408
 
@@ -410,36 +285,12 @@ def set_config_value(key: str, value: Any):
410
  """Set a config value."""
411
  config.set(key, value)
412
 
413
- def get_gcp_project_id() -> Optional[str]:
414
- return config.get("gcp_project_id")
415
-
416
- # ------------------ CLI Test ------------------
417
-
418
- if __name__ == "__main__":
419
- print("\n=== Unified Config Test ===\n")
420
- try:
421
- conf = load_configuration()
422
- print("Configuration Loaded Successfully!")
423
- print(f"Setup Name: {conf.get('setup_name')}")
424
- print(f"Project ID: {conf.get('gcp_project_id')}")
425
-
426
- # Check required keys
427
- required = ["gemini_api_key", "runwayml_api_key", "gcs_bucket_name", "gcp_project_id"]
428
- missing = [k for k in required if not conf.get(k)]
429
-
430
- if missing:
431
- print(f"\n[WARNING] Missing keys: {missing}")
432
- else:
433
- print("\nAll required keys present.")
434
-
435
- except Exception as e:
436
- print(f"\n[ERROR] {e}")
437
-
438
 
439
  def configure_job_environment(job_index: int):
440
  """
441
- Configure environment variables and config settings specific to a parallel job index.
442
- Primarily handles API key rotation for Google AI Studio.
443
  """
444
  if job_index is None:
445
  return
@@ -452,13 +303,10 @@ def configure_job_environment(job_index: int):
452
  "GOOGLE_AI_API_KEY"
453
  ]
454
 
455
- # Ensure RUNWAYML_API_KEY is robust
456
  os.environ["RUNWAYML_API_KEY"] = os.getenv("RUNWAYML_API_KEY", "")
457
 
458
- # Select key based on job index modulo
459
  selected_key = google_keys_env[job_index % len(google_keys_env)]
460
 
461
- # Update both env and specific config
462
  new_api_key = os.getenv(selected_key, "")
463
  if new_api_key:
464
  os.environ["GEMINI_API_KEY"] = new_api_key
@@ -466,3 +314,25 @@ def configure_job_environment(job_index: int):
466
  print(f"Using Google key: {selected_key}")
467
  else:
468
  print(f"Warning: Selected key {selected_key} is empty or not set.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  """
2
  Unified Configuration Module
3
 
4
+ Simple .env-based configuration. No TOML files needed.
5
+ All configuration comes from environment variables.
6
  """
7
 
8
  import os
 
9
  import json
10
  import logging
11
  from pathlib import Path
 
18
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
19
  logger = logging.getLogger(__name__)
20
 
 
 
 
 
 
 
 
 
 
21
  # ------------------ Singleton & Cache ------------------
22
 
23
  _cached_config: Optional[Dict[str, Any]] = None
 
24
  _config_initialized: bool = False
25
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
 
27
  # ------------------ GCP Auth Configuration ------------------
28
 
 
40
  os.getenv("GOOGLE_APPLICATION_CREDENTIALS"))
41
 
42
  if gcp_creds_path:
 
43
  os.environ["MY_TEMP_GCS_BUCKET"] = os.getenv("MY_TEMP_GCS_BUCKET", "")
44
 
45
  try:
 
62
  # 2. Workload Identity Federation / ADC
63
  if not gcp_project_id:
64
  try:
 
65
  creds, project = default()
66
  if project:
67
  gcp_project_id = project
 
106
  return gcp_project_id, auth_method
107
 
108
 
109
+ # ------------------ Helper to get bool from env ------------------
110
+
111
+ def _env_bool(key: str, default: bool = False) -> bool:
112
+ """Get boolean from environment variable."""
113
+ return os.getenv(key, str(default)).lower() in ("true", "1", "yes")
114
+
115
+
116
+ def _env_int(key: str, default: int = 0) -> int:
117
+ """Get integer from environment variable."""
118
+ try:
119
+ return int(os.getenv(key, str(default)))
120
+ except ValueError:
121
+ return default
122
+
123
+
124
  # ------------------ Main Load Function ------------------
125
 
126
  def load_configuration(force_reload: bool = False) -> Dict[str, Any]:
127
  """
128
+ Load configuration from environment variables.
129
 
130
  1. Load .env
131
+ 2. Resolve GCP Project & Secrets
132
+ 3. Build config dict
 
133
  """
134
+ global _cached_config, _config_initialized
135
 
 
 
136
  # Return cache if valid
137
+ if _config_initialized and not force_reload and _cached_config is not None:
 
 
138
  return _cached_config
139
 
140
  load_dotenv()
141
 
142
+ setup_type = os.getenv("SETUP_TYPE")
143
+ logger.info(f"βœ“ Loaded setup config: {setup_type}")
 
 
 
 
144
 
145
  # Resolve GCP Project
146
  gcp_project_id, auth_method = _resolve_gcp_project_id()
147
  if gcp_project_id:
148
  logger.info(f"βœ“ GCP Project ID: {gcp_project_id} ({auth_method})")
149
 
150
+ # Build config from environment variables
151
  config = {
152
+ # Core settings
153
+ "setup_type": setup_type,
 
 
154
  "gcp_project_id": gcp_project_id,
155
  "auth_method": auth_method,
156
+
157
+ # API Keys
158
+ "gemini_api_key": os.getenv("GEMINI_API_KEY"),
159
+ "runwayml_api_key": os.getenv("RUNWAYML_API_KEY"),
160
  "a2e_api_key": os.getenv("A2E_API_KEY"),
161
+
162
+ # GCS
163
+ "gcs_bucket_name": os.getenv("GCS_BUCKET_NAME"),
164
+
165
+ # Google Sheets
166
+ "gsheet_name": os.getenv("GSHEET_NAME"),
167
+ "gsheet_id": os.getenv("GSHEET_ID"),
168
+ "video_library_worksheet": os.getenv("VIDEO_LIBRARY_GSHEET_WORKSHEET"),
169
+ "audio_library_worksheet": os.getenv("AUDIO_LIBRARY_GSHEET_WORKSHEET"),
170
+ "logs_worksheet": os.getenv("GSHEET_WORKSHEET_LOGS"),
171
+ "content_strategy_worksheet": os.getenv("CONTENT_STRATEGY_GSHEET_WORKSHEET"),
172
+
173
+ # Pipeline settings
174
+ "caption_style": os.getenv("CAPTION_STYLE"),
175
+ "beat_method": os.getenv("BEAT_METHOD"),
176
+ "on_screen_text": _env_bool("ON_SCREEN_TEXT"),
177
+ "is_onscreen_cta": _env_bool("IS_ONSCREEN_CTA"),
178
+ "is_a2e_lip_sync": _env_bool("IS_A2E_LIP_SYNC"),
179
+ "use_1x1_ratio": _env_bool("USE_1X1_RATIO"),
180
+ "use_veo": _env_bool("USE_VEO"),
181
+ "use_gemimi_video": _env_bool("USE_GEMIMI_VIDEO"),
182
+ "only_random_videos": _env_bool("ONLY_RANDOM_VIDEOS"),
183
+ "plain_video_count": _env_int("PlAIN_VIDEO_COUNT", 100),
184
+
185
+ # Test settings
186
+ "test_automation": _env_bool("TEST_AUTOMATION"),
187
  "test_data_directory": os.getenv("TEST_DATA_DIRECTORY"),
188
+ "delete_all_a2e_videos": _env_bool("DELETE_ALL_A2E_VIDEOS"),
189
+
190
+ # TikTok
191
  "tiktok_access_token": os.getenv("TIKTOK_ACCESS_TOKEN"),
192
  "tiktok_privacy_level": os.getenv("TIKTOK_PRIVACY_LEVEL", "PUBLIC_TO_EVERYONE"),
193
+
194
+ # YouTube
195
  "youtube_refresh_token": os.getenv("YOUTUBE_REFRESH_TOKEN"),
196
  "youtube_client_secrets_json": os.getenv("YOUTUBE_CLIENT_SECRETS_JSON"),
197
  "youtube_client_secrets_base64": os.getenv("YOUTUBE_CLIENT_SECRETS_BASE64"),
 
200
  "video_category": os.getenv("VIDEO_CATEGORY", "22"),
201
  "video_privacy": os.getenv("VIDEO_PRIVACY", "private"),
202
  "scheduled_time": os.getenv("SCHEDULED_TIME"),
203
+
204
+ # Instagram
205
  "instagram_access_token": os.getenv("INSTAGRAM_ACCESS_TOKEN"),
206
  "instagram_account_id": os.getenv("INSTAGRAM_ACCOUNT_ID"),
207
+
208
+ # Misc
209
+ "commit_progress": _env_bool("COMMIT_PROGRESS"),
210
  }
211
 
212
+ # On-screen CTA options
213
  config["on_screen_cta"] = [
214
  "LINK IN BIO πŸ›οΈ",
215
  "πŸ”— LINK IN ACCOUNT πŸ›οΈ",
 
219
  "πŸ›οΈ SALE ENDING SOON - πŸ”— LINK IN BIO"
220
  ]
221
 
222
+ # Initialize default empty collections
223
  config["video_usage_count"] = {}
224
  config["avatar_usage_count"] = {}
225
  config["visual_assets"] = {}
226
 
 
 
227
  _cached_config = config
 
228
  _config_initialized = True
229
 
230
  return config
 
277
 
278
 
279
  def get_config_value(key: str, default: Any = None) -> Any:
280
+ """Get a config value."""
 
 
281
  value = config.get(key, default)
282
  return value if value is not None else default
283
 
 
285
  """Set a config value."""
286
  config.set(key, value)
287
 
288
+ # ------------------ Job Environment Configuration ------------------
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
289
 
290
  def configure_job_environment(job_index: int):
291
  """
292
+ Configure environment variables for a parallel job index.
293
+ Handles API key rotation for Google AI Studio.
294
  """
295
  if job_index is None:
296
  return
 
303
  "GOOGLE_AI_API_KEY"
304
  ]
305
 
 
306
  os.environ["RUNWAYML_API_KEY"] = os.getenv("RUNWAYML_API_KEY", "")
307
 
 
308
  selected_key = google_keys_env[job_index % len(google_keys_env)]
309
 
 
310
  new_api_key = os.getenv(selected_key, "")
311
  if new_api_key:
312
  os.environ["GEMINI_API_KEY"] = new_api_key
 
314
  print(f"Using Google key: {selected_key}")
315
  else:
316
  print(f"Warning: Selected key {selected_key} is empty or not set.")
317
+
318
+
319
+ # ------------------ CLI Test ------------------
320
+
321
+ if __name__ == "__main__":
322
+ print("\n=== Config Test ===\n")
323
+ try:
324
+ conf = load_configuration()
325
+ print("Configuration Loaded!")
326
+ print(f"Setup Type: {conf.get('setup_type')}")
327
+ print(f"Project ID: {conf.get('gcp_project_id')}")
328
+
329
+ required = ["gemini_api_key", "gcs_bucket_name", "gcp_project_id"]
330
+ missing = [k for k in required if not conf.get(k)]
331
+
332
+ if missing:
333
+ print(f"\n[WARNING] Missing: {missing}")
334
+ else:
335
+ print("\nβœ“ All required keys present.")
336
+
337
+ except Exception as e:
338
+ print(f"\n[ERROR] {e}")