voxkit.config
This module provides access to configurable info about the application.
Configurations
- App config: application metadata and provenance (
app_config). - Pipeline config: pipeline steps and UI wiring (
pipeline_config). - Startup config: launch-time constants and defaults (
startup_config). - Logging config: rotating file logger setup (
logging_config).
Only app_config and pipeline_config are dynamic post-build; they are
loaded from YAML files under the active profile and can change without a
rebuild. startup_config and logging_config are baked in at build time.
Profile System
Configs are organized into profiles under config/profiles/
Use get_active_profile() to check which profile is active. Use get_profile_config_path() to get the path to the active profile's directory.
1"""This module provides access to configurable info about the application. 2 3Configurations 4-------------- 5- App config: application metadata and provenance (``app_config``). 6- Pipeline config: pipeline steps and UI wiring (``pipeline_config``). 7- Startup config: launch-time constants and defaults (``startup_config``). 8- Logging config: rotating file logger setup (``logging_config``). 9 10Only ``app_config`` and ``pipeline_config`` are dynamic post-build; they are 11loaded from YAML files under the active profile and can change without a 12rebuild. ``startup_config`` and ``logging_config`` are baked in at build time. 13 14Profile System 15-------------- 16Configs are organized into profiles under config/profiles/<name>/. 17The active profile is specified in config/profile.txt. 18 19Use get_active_profile() to check which profile is active. 20Use get_profile_config_path() to get the path to the active profile's directory. 21""" 22 23from voxkit.config.app_config import ( 24 AppConfig, 25 get_active_profile, 26 get_app_config, 27 get_config_root, 28 get_profile_config_path, 29 resolve_config_file, 30) 31from voxkit.config.constants import DEFAULT_HELP_URL 32from voxkit.config.logging_config import ( 33 LOG_FILE, 34 reset_logging, 35 setup_logging, 36) 37from voxkit.config.pipeline_config import ( 38 PipelineConfig, 39 PipelineStep, 40 UIConfig, 41 get_pipeline_config, 42) 43from voxkit.config.startup_config import ( 44 STARTUP_SCRIPT, 45 AppName, 46 Defaults, 47 Dimensions, 48 Mode, 49) 50 51__all__ = [ 52 # Profile system 53 "get_active_profile", 54 "get_config_root", 55 "get_profile_config_path", 56 "resolve_config_file", 57 # App config 58 "AppConfig", 59 "get_app_config", 60 # Pipeline config 61 "PipelineConfig", 62 "PipelineStep", 63 "UIConfig", 64 "get_pipeline_config", 65 # Startup config 66 "DEFAULT_HELP_URL", 67 "AppName", 68 "Dimensions", 69 "Defaults", 70 "Mode", 71 "STARTUP_SCRIPT", 72 # Logging config 73 "LOG_FILE", 74 "setup_logging", 75 "reset_logging", 76]
40def get_active_profile() -> str: 41 """Get the active configuration profile name. 42 43 Reads from config/profile.txt. Falls back to 'default' if file doesn't exist. 44 45 Returns: 46 Profile name string 47 """ 48 config_root = get_config_root() 49 profile_file = config_root / "profile.txt" 50 51 if profile_file.exists(): 52 return profile_file.read_text().strip() 53 return "default"
Get the active configuration profile name.
Reads from config/profile.txt. Falls back to 'default' if file doesn't exist.
Returns: Profile name string
21def get_config_root() -> Path: 22 """Get the path to the config root directory. 23 24 Returns the correct config path whether running from source or as a 25 PyInstaller bundle. 26 27 Returns: 28 Path to the config directory 29 """ 30 # Check if running as PyInstaller bundle 31 if getattr(sys, "_MEIPASS", None): 32 # Running as bundled executable 33 # mypy: ignore attr-defined on _MEIPASS - it's dynamically added by PyInstaller 34 return Path(getattr(sys, "_MEIPASS")) / "config" 35 else: 36 # Running from source - get project root (3 levels up from this file) 37 return Path(__file__).parent.parent.parent.parent / "config"
Get the path to the config root directory.
Returns the correct config path whether running from source or as a PyInstaller bundle.
Returns: Path to the config directory
56def get_profile_config_path() -> Path: 57 """Get the path to the active profile's config directory. 58 59 Returns: 60 Path to the active profile directory (e.g., config/profiles/default/) 61 """ 62 config_root = get_config_root() 63 profile = get_active_profile() 64 profile_path = config_root / "profiles" / profile 65 66 # Fall back to legacy location if profile doesn't exist 67 if not profile_path.exists(): 68 return config_root 69 70 return profile_path
Get the path to the active profile's config directory.
Returns: Path to the active profile directory (e.g., config/profiles/default/)
73def resolve_config_file(filename: str) -> Path: 74 """Resolve a config file path with fallback to default profile. 75 76 Looks for the file in the active profile first, then falls back to 77 the default profile if not found. This allows profiles to only 78 override the files they need to change. 79 80 Args: 81 filename: The config file name (e.g., "app_info.yaml") 82 83 Returns: 84 Path to the config file (from active profile or default) 85 86 Raises: 87 FileNotFoundError: If file not found in active or default profile 88 """ 89 config_root = get_config_root() 90 profile = get_active_profile() 91 92 # Try active profile first 93 active_path = config_root / "profiles" / profile / filename 94 if active_path.exists(): 95 return active_path 96 97 # Fall back to default profile 98 default_path = config_root / "profiles" / "default" / filename 99 if default_path.exists(): 100 return default_path 101 102 # Throw error if not found in either location 103 raise FileNotFoundError( 104 f"Config file '{filename}' not found in profile '{profile}' or default profile" 105 )
Resolve a config file path with fallback to default profile.
Looks for the file in the active profile first, then falls back to the default profile if not found. This allows profiles to only override the files they need to change.
Args: filename: The config file name (e.g., "app_info.yaml")
Returns: Path to the config file (from active profile or default)
Raises: FileNotFoundError: If file not found in active or default profile
108@dataclass 109class AppConfig: 110 """Application configuration data class.""" 111 112 app_name: str 113 version: str 114 description: str 115 introduction: str 116 help_url: str | None = None 117 feedback_email: str | None = None 118 release_date: Optional[str] = None 119 release_notes: Optional[str] = None 120 log_max_bytes: int = 5 * 1024 * 1024 121 log_backup_count: int = 3 122 123 @classmethod 124 def from_yaml(cls, config_path: Path) -> "AppConfig": 125 """Load application configuration from YAML file. 126 127 Args: 128 config_path: Path to the app_info.yaml file 129 130 Returns: 131 AppConfig instance with loaded configuration 132 133 Raises: 134 FileNotFoundError: If config file doesn't exist 135 yaml.YAMLError: If YAML parsing fails 136 """ 137 if not config_path.exists(): 138 raise FileNotFoundError(f"App config file not found: {config_path}") 139 140 with open(config_path, "r", encoding="utf-8") as f: 141 data = yaml.safe_load(f) or {} 142 contact_info = data.get("contact_info", {}) 143 144 # Version is sourced from config/VERSION (single source of truth), 145 # not from per-profile YAML. 146 version_file = get_config_root() / "VERSION" 147 version = version_file.read_text(encoding="utf-8").strip() 148 149 return cls( 150 app_name=data.get("app_name", "VoxKit"), 151 version=version, 152 description=data.get("description", ""), 153 introduction=data.get("introduction", ""), 154 help_url=data.get("help_url", DEFAULT_HELP_URL), 155 feedback_email=data.get("feedback_email") or contact_info.get("email_support"), 156 release_date=data.get("release_date"), 157 release_notes=data.get("release_notes"), 158 log_max_bytes=int(data.get("log_max_bytes", 5 * 1024 * 1024)), 159 log_backup_count=int(data.get("log_backup_count", 3)), 160 ) 161 162 @classmethod 163 def load_default(cls) -> "AppConfig": 164 """Load the application configuration from the active profile. 165 166 Loads from config/profiles/<active_profile>/app_info.yaml. 167 Falls back to default profile or config root if not found. 168 169 Returns: 170 AppConfig instance 171 """ 172 config_path = resolve_config_file("app_info.yaml") 173 return cls.from_yaml(config_path)
Application configuration data class.
123 @classmethod 124 def from_yaml(cls, config_path: Path) -> "AppConfig": 125 """Load application configuration from YAML file. 126 127 Args: 128 config_path: Path to the app_info.yaml file 129 130 Returns: 131 AppConfig instance with loaded configuration 132 133 Raises: 134 FileNotFoundError: If config file doesn't exist 135 yaml.YAMLError: If YAML parsing fails 136 """ 137 if not config_path.exists(): 138 raise FileNotFoundError(f"App config file not found: {config_path}") 139 140 with open(config_path, "r", encoding="utf-8") as f: 141 data = yaml.safe_load(f) or {} 142 contact_info = data.get("contact_info", {}) 143 144 # Version is sourced from config/VERSION (single source of truth), 145 # not from per-profile YAML. 146 version_file = get_config_root() / "VERSION" 147 version = version_file.read_text(encoding="utf-8").strip() 148 149 return cls( 150 app_name=data.get("app_name", "VoxKit"), 151 version=version, 152 description=data.get("description", ""), 153 introduction=data.get("introduction", ""), 154 help_url=data.get("help_url", DEFAULT_HELP_URL), 155 feedback_email=data.get("feedback_email") or contact_info.get("email_support"), 156 release_date=data.get("release_date"), 157 release_notes=data.get("release_notes"), 158 log_max_bytes=int(data.get("log_max_bytes", 5 * 1024 * 1024)), 159 log_backup_count=int(data.get("log_backup_count", 3)), 160 )
Load application configuration from YAML file.
Args: config_path: Path to the app_info.yaml file
Returns: AppConfig instance with loaded configuration
Raises: FileNotFoundError: If config file doesn't exist yaml.YAMLError: If YAML parsing fails
162 @classmethod 163 def load_default(cls) -> "AppConfig": 164 """Load the application configuration from the active profile. 165 166 Loads from config/profiles/<active_profile>/app_info.yaml. 167 Falls back to default profile or config root if not found. 168 169 Returns: 170 AppConfig instance 171 """ 172 config_path = resolve_config_file("app_info.yaml") 173 return cls.from_yaml(config_path)
Load the application configuration from the active profile.
Loads from config/profiles/
Returns: AppConfig instance
176def get_app_config() -> AppConfig: 177 """Get the application configuration. 178 179 Convenience function to load the default configuration. 180 181 Returns: 182 AppConfig instance 183 """ 184 return AppConfig.load_default()
Get the application configuration.
Convenience function to load the default configuration.
Returns: AppConfig instance
90@dataclass 91class PipelineConfig: 92 """Pipeline configuration data class.""" 93 94 steps: List[PipelineStep] 95 ui_config: UIConfig 96 97 @property 98 def enabled_steps(self) -> List[PipelineStep]: 99 """Get only the enabled pipeline steps. 100 101 Returns: 102 List of enabled PipelineStep instances 103 """ 104 return [step for step in self.steps if step.enabled] 105 106 @classmethod 107 def from_yaml(cls, config_path: Path) -> "PipelineConfig": 108 """Load pipeline configuration from YAML file. 109 110 Args: 111 config_path: Path to the pipeline_definitions.yaml file 112 113 Returns: 114 PipelineConfig instance with loaded configuration 115 116 Raises: 117 FileNotFoundError: If config file doesn't exist 118 yaml.YAMLError: If YAML parsing fails 119 """ 120 if not config_path.exists(): 121 raise FileNotFoundError(f"Pipeline config file not found: {config_path}") 122 123 with open(config_path, "r", encoding="utf-8") as f: 124 data = yaml.safe_load(f) 125 126 # Parse pipeline steps 127 steps = [] 128 for step_data in data.get("pipeline", []): 129 steps.append(PipelineStep.from_dict(step_data)) 130 131 # Parse UI config 132 ui_config = UIConfig.from_dict(data.get("ui")) 133 134 return cls(steps=steps, ui_config=ui_config) 135 136 @classmethod 137 def load_default(cls) -> "PipelineConfig": 138 """Load the pipeline configuration from the active profile. 139 140 Loads from config/profiles/<active_profile>/pipeline_definitions.yaml. 141 Falls back to default profile or config root if not found. 142 143 Returns: 144 PipelineConfig instance 145 """ 146 config_path = resolve_config_file("pipeline_definitions.yaml") 147 return cls.from_yaml(config_path)
Pipeline configuration data class.
97 @property 98 def enabled_steps(self) -> List[PipelineStep]: 99 """Get only the enabled pipeline steps. 100 101 Returns: 102 List of enabled PipelineStep instances 103 """ 104 return [step for step in self.steps if step.enabled]
Get only the enabled pipeline steps.
Returns: List of enabled PipelineStep instances
106 @classmethod 107 def from_yaml(cls, config_path: Path) -> "PipelineConfig": 108 """Load pipeline configuration from YAML file. 109 110 Args: 111 config_path: Path to the pipeline_definitions.yaml file 112 113 Returns: 114 PipelineConfig instance with loaded configuration 115 116 Raises: 117 FileNotFoundError: If config file doesn't exist 118 yaml.YAMLError: If YAML parsing fails 119 """ 120 if not config_path.exists(): 121 raise FileNotFoundError(f"Pipeline config file not found: {config_path}") 122 123 with open(config_path, "r", encoding="utf-8") as f: 124 data = yaml.safe_load(f) 125 126 # Parse pipeline steps 127 steps = [] 128 for step_data in data.get("pipeline", []): 129 steps.append(PipelineStep.from_dict(step_data)) 130 131 # Parse UI config 132 ui_config = UIConfig.from_dict(data.get("ui")) 133 134 return cls(steps=steps, ui_config=ui_config)
Load pipeline configuration from YAML file.
Args: config_path: Path to the pipeline_definitions.yaml file
Returns: PipelineConfig instance with loaded configuration
Raises: FileNotFoundError: If config file doesn't exist yaml.YAMLError: If YAML parsing fails
136 @classmethod 137 def load_default(cls) -> "PipelineConfig": 138 """Load the pipeline configuration from the active profile. 139 140 Loads from config/profiles/<active_profile>/pipeline_definitions.yaml. 141 Falls back to default profile or config root if not found. 142 143 Returns: 144 PipelineConfig instance 145 """ 146 config_path = resolve_config_file("pipeline_definitions.yaml") 147 return cls.from_yaml(config_path)
Load the pipeline configuration from the active profile.
Loads from config/profiles/
Returns: PipelineConfig instance
20@dataclass 21class PipelineStep: 22 """Represents a single step in the pipeline.""" 23 24 id: str 25 label: str 26 stacker_class: str 27 enabled: bool = True 28 collapsible_sections: Optional[Dict[str, str]] = None # {header: content} pairs 29 markdown_content: Optional[str] = None # For MarkdownStacker 30 31 @classmethod 32 def from_dict(cls, data: Dict[str, Any]) -> "PipelineStep": 33 """Create a PipelineStep from a dictionary. 34 35 Args: 36 data: Dictionary containing step configuration 37 38 Returns: 39 PipelineStep instance 40 """ 41 # Handle both old format (description/info) and new format (collapsible_sections) 42 collapsible_sections = data.get("collapsible_sections") 43 44 # Backwards compatibility: convert old description/info fields to collapsible_sections 45 if collapsible_sections is None and ("description" in data or "info" in data): 46 collapsible_sections = {} 47 if "description" in data and data["description"]: 48 collapsible_sections["Step Instructions"] = data["description"] 49 if "info" in data and data["info"]: 50 collapsible_sections["Additional Info"] = data["info"] 51 52 return cls( 53 id=data["id"], 54 label=data["label"], 55 stacker_class=data["stacker_class"], 56 enabled=data.get("enabled", True), 57 collapsible_sections=collapsible_sections, 58 markdown_content=data.get("markdown_content"), 59 )
Represents a single step in the pipeline.
31 @classmethod 32 def from_dict(cls, data: Dict[str, Any]) -> "PipelineStep": 33 """Create a PipelineStep from a dictionary. 34 35 Args: 36 data: Dictionary containing step configuration 37 38 Returns: 39 PipelineStep instance 40 """ 41 # Handle both old format (description/info) and new format (collapsible_sections) 42 collapsible_sections = data.get("collapsible_sections") 43 44 # Backwards compatibility: convert old description/info fields to collapsible_sections 45 if collapsible_sections is None and ("description" in data or "info" in data): 46 collapsible_sections = {} 47 if "description" in data and data["description"]: 48 collapsible_sections["Step Instructions"] = data["description"] 49 if "info" in data and data["info"]: 50 collapsible_sections["Additional Info"] = data["info"] 51 52 return cls( 53 id=data["id"], 54 label=data["label"], 55 stacker_class=data["stacker_class"], 56 enabled=data.get("enabled", True), 57 collapsible_sections=collapsible_sections, 58 markdown_content=data.get("markdown_content"), 59 )
Create a PipelineStep from a dictionary.
Args: data: Dictionary containing step configuration
Returns: PipelineStep instance
62@dataclass 63class UIConfig: 64 """UI-related configuration.""" 65 66 menu_max_width: int = 500 67 animation_duration: int = 300 68 content_spacing: int = 20 69 70 @classmethod 71 def from_dict(cls, data: Optional[Dict[str, Any]]) -> "UIConfig": 72 """Create a UIConfig from a dictionary. 73 74 Args: 75 data: Dictionary containing UI configuration or None 76 77 Returns: 78 UIConfig instance with values from dict or defaults 79 """ 80 if data is None: 81 return cls() 82 83 return cls( 84 menu_max_width=data.get("menu_max_width", 500), 85 animation_duration=data.get("animation_duration", 300), 86 content_spacing=data.get("content_spacing", 20), 87 )
UI-related configuration.
70 @classmethod 71 def from_dict(cls, data: Optional[Dict[str, Any]]) -> "UIConfig": 72 """Create a UIConfig from a dictionary. 73 74 Args: 75 data: Dictionary containing UI configuration or None 76 77 Returns: 78 UIConfig instance with values from dict or defaults 79 """ 80 if data is None: 81 return cls() 82 83 return cls( 84 menu_max_width=data.get("menu_max_width", 500), 85 animation_duration=data.get("animation_duration", 300), 86 content_spacing=data.get("content_spacing", 20), 87 )
Create a UIConfig from a dictionary.
Args: data: Dictionary containing UI configuration or None
Returns: UIConfig instance with values from dict or defaults
150def get_pipeline_config() -> PipelineConfig: 151 """Get the pipeline configuration. 152 153 Convenience function to load the default configuration. 154 155 Returns: 156 PipelineConfig instance 157 """ 158 return PipelineConfig.load_default()
Get the pipeline configuration.
Convenience function to load the default configuration.
Returns: PipelineConfig instance
29def startup_routine(): 30 """Example startup routine to be executed on first launch.""" 31 log.info("Initializing VoxKit...") 32 time.sleep(1) # Simulate initialization 33 34 storage_root = get_storage_root() 35 log.info("Storage root: %s", storage_root) 36 37 log.info("Creating required directories...") 38 (storage_root / "computed-likelihoods").mkdir(parents=True, exist_ok=True) 39 (storage_root / "custom-likelihoods").mkdir(parents=True, exist_ok=True) 40 time.sleep(1) # Simulate directory setup 41 42 # Download MFA models 43 log.info("Downloading MFA models...") 44 mfa_models = [ 45 "acoustic-english_us_arpa-v3.0.0/english_us_arpa.zip", 46 "acoustic-spanish_mfa-v3.3.0/spanish_mfa.zip", 47 ] 48 mfa_models_path = storage_root / "MFAENGINE" / MODELS_ROOT 49 mfa_models_path.mkdir(parents=True, exist_ok=True) 50 for model in mfa_models: 51 success, metadata = models.create_model( 52 "MFAENGINE", model.split("/")[1].replace(".zip", "") 53 ) 54 if not success: 55 log.error("Failed to create model metadata for %s: %s", model, metadata) 56 continue 57 assert not isinstance(metadata, str) 58 model_dest = metadata.get("model_path") 59 if not model_dest: 60 log.error("Model path not found in metadata for %s", model) 61 continue 62 63 # Remove last part of path and relace with .zip 64 output_file = model_dest.parent / model.split("/")[1] 65 66 try: 67 download_acoustic_model(model, str(output_file)) 68 # Update metadata to reflect downloaded file 69 success, message = models.update_model_metadata( 70 "MFAENGINE", metadata["id"], {"model_path": str(output_file)} 71 ) 72 73 if not success: 74 log.error("Failed to update model metadata for %s: %s", model, message) 75 76 log.info("MFA model %s downloaded to %s", model, output_file) 77 except Exception: 78 log.exception("Failed to download MFA model %s", model) 79 80 # Download W2TG model from HuggingFace 81 log.info("Downloading W2TG model from HuggingFace...") 82 # Create folder for W2TG model 83 w2tg_path = storage_root / "W2TGENGINE" / MODELS_ROOT 84 w2tg_path.mkdir(parents=True, exist_ok=True) 85 success, metadata = models.create_model("W2TGENGINE", "default") 86 if not success: 87 log.error("Failed to create W2TG model metadata: %s", metadata) 88 return 89 assert not isinstance(metadata, str) 90 model_dest = metadata.get("model_path") 91 if not model_dest: 92 log.error("Model path not found in W2TG metadata") 93 return 94 result = download_and_copy_huggingface_model( 95 model_path="pkadambi/Wav2TextGrid", 96 destination=str(model_dest), 97 ) 98 if result: 99 log.info("W2TG model downloaded to %s", result) 100 else: 101 log.error("Failed to download W2TG model") 102 103 try: 104 import nltk 105 106 nltk.download("averaged_perceptron_tagger_eng") 107 108 except Exception: 109 log.exception("Failed to download NLTK resources") 110 111 log.info("Initialization complete")
Example startup routine to be executed on first launch.
27def setup_logging( 28 max_bytes: int = DEFAULT_MAX_BYTES, 29 backup_count: int = DEFAULT_BACKUP_COUNT, 30 log_file: Optional[Path] = None, 31) -> RotatingFileHandler: 32 """Configure the root logger with a rotating file handler. 33 34 Idempotent — calling more than once has no effect beyond the first call. 35 36 Args: 37 max_bytes: Max size in bytes before rotation. 38 backup_count: Number of rotated files to retain. 39 log_file: Override the log file path (primarily for tests). 40 41 Returns: 42 The installed RotatingFileHandler. 43 """ 44 global _configured 45 46 target = log_file or LOG_FILE 47 target.parent.mkdir(parents=True, exist_ok=True) 48 49 root = logging.getLogger() 50 debug_enabled = os.environ.get("VOXKIT_DEBUG") == "1" 51 root.setLevel(logging.DEBUG if debug_enabled else logging.INFO) 52 53 if _configured: 54 for handler in root.handlers: 55 if isinstance(handler, RotatingFileHandler): 56 return handler 57 58 handler = RotatingFileHandler( 59 target, 60 maxBytes=max_bytes, 61 backupCount=backup_count, 62 encoding="utf-8", 63 ) 64 handler.setFormatter(logging.Formatter(_LOG_FORMAT, datefmt=_DATE_FORMAT)) 65 handler.setLevel(logging.DEBUG if debug_enabled else logging.INFO) 66 root.addHandler(handler) 67 68 _configured = True 69 logging.getLogger(__name__).info( 70 "Logging initialized (debug=%s, file=%s)", debug_enabled, target 71 ) 72 return handler
Configure the root logger with a rotating file handler.
Idempotent — calling more than once has no effect beyond the first call.
Args: max_bytes: Max size in bytes before rotation. backup_count: Number of rotated files to retain. log_file: Override the log file path (primarily for tests).
Returns: The installed RotatingFileHandler.
75def reset_logging() -> None: 76 """Remove handlers installed by :func:`setup_logging`. Test helper.""" 77 global _configured 78 root = logging.getLogger() 79 for handler in list(root.handlers): 80 root.removeHandler(handler) 81 handler.close() 82 _configured = False
Remove handlers installed by setup_logging(). Test helper.