Configuration reference

audiotext.config

AppConfig dataclass

Source code in src/audiotext/config.py
@dataclass(frozen=True)
class AppConfig:
    environment: str = "development"
    host: str = "127.0.0.1"
    port: int = 8791
    data_dir: Path = field(default_factory=lambda: default_data_dir())
    database_url: str | None = None
    public_base_url: str = "http://127.0.0.1:8791"
    token_pepper: str = DEV_SECRET
    admin_session_secret: str = DEV_SECRET
    admin_cidr_allowlist: tuple[str, ...] = ("127.0.0.1/32", "::1/128")
    cors_origins: tuple[str, ...] = ()
    max_upload_bytes: int = 25 * 1024 * 1024
    max_sync_audio_seconds: int = 120
    max_async_audio_seconds: int = 900
    max_audio_channels: int = 2
    max_sample_rate_hz: int = 48_000
    audio_probe_timeout_seconds: int = 15
    max_process_rss_bytes: int = 0
    max_concurrent_transcriptions: int = 1
    max_model_downloads: int = 1
    max_running_jobs: int = 1
    max_queued_jobs: int = 100
    default_max_concurrent_async_jobs: int = 3
    model_idle_ttl_seconds: int = 900
    max_loaded_models: int = 1
    default_async_retention_hours: int = 24
    output_cache_enabled: bool = False
    request_timeout_seconds: int = 900
    async_job_runner: str = "background"
    cleanup_interval_seconds: int = 3600
    orphan_upload_retention_seconds: int = 3600
    audit_retention_days: int = 90
    preload_models: tuple[str, ...] = ()
    warmup_audio_path: str | None = None
    allow_trust_remote_code: bool = False

    @property
    def resolved_database_url(self) -> str:
        if self.database_url:
            return self.database_url
        return f"sqlite:///{self.data_dir / 'audiotext.sqlite3'}"

    @property
    def is_production(self) -> bool:
        return self.environment.lower() in {"prod", "production"}

    def ensure_directories(self) -> None:
        self.data_dir.mkdir(parents=True, exist_ok=True)
        (self.data_dir / "uploads").mkdir(parents=True, exist_ok=True)
        (self.data_dir / "results").mkdir(parents=True, exist_ok=True)
        (self.data_dir / "logs").mkdir(parents=True, exist_ok=True)

    def validate_for_startup(self) -> None:
        if self.port < 1 or self.port > 65535:
            raise ValueError("port must be between 1 and 65535")
        if self.max_upload_bytes < 1:
            raise ValueError("max_upload_bytes must be positive")
        if self.audio_probe_timeout_seconds < 1:
            raise ValueError("audio_probe_timeout_seconds must be positive")
        if self.max_audio_channels < 1:
            raise ValueError("max_audio_channels must be positive")
        if self.max_sample_rate_hz < 1:
            raise ValueError("max_sample_rate_hz must be positive")
        if self.max_process_rss_bytes < 0:
            raise ValueError("max_process_rss_bytes must be zero or positive")
        if self.max_concurrent_transcriptions < 1:
            raise ValueError("max_concurrent_transcriptions must be positive")
        if self.max_model_downloads < 1:
            raise ValueError("max_model_downloads must be positive")
        if self.max_running_jobs < 1:
            raise ValueError("max_running_jobs must be positive")
        if self.max_queued_jobs < 1:
            raise ValueError("max_queued_jobs must be positive")
        if self.default_max_concurrent_async_jobs < 1:
            raise ValueError("default_max_concurrent_async_jobs must be positive")
        if self.max_loaded_models < 1:
            raise ValueError("max_loaded_models must be positive")
        if self.async_job_runner not in {"background", "external"}:
            raise ValueError("async_job_runner must be 'background' or 'external'")
        if self.cleanup_interval_seconds < 0:
            raise ValueError("cleanup_interval_seconds must be zero or positive")
        if self.orphan_upload_retention_seconds < 1:
            raise ValueError("orphan_upload_retention_seconds must be positive")
        if self.audit_retention_days < 1:
            raise ValueError("audit_retention_days must be positive")
        if self.is_production:
            missing = []
            if not self.token_pepper or self.token_pepper == DEV_SECRET:
                missing.append(f"{ENV_PREFIX}TOKEN_PEPPER")
            if not self.admin_session_secret or self.admin_session_secret == DEV_SECRET:
                missing.append(f"{ENV_PREFIX}ADMIN_SESSION_SECRET")
            if missing:
                raise ValueError("production requires configured secrets: " + ", ".join(missing))

    def safe_dict(self) -> dict[str, object]:
        payload = asdict(self)
        payload["data_dir"] = str(self.data_dir)
        payload["database_url"] = redact_secret(self.resolved_database_url)
        payload["token_pepper"] = configured_state(self.token_pepper)
        payload["admin_session_secret"] = configured_state(self.admin_session_secret)
        return payload

environment class-attribute instance-attribute

environment = 'development'

host class-attribute instance-attribute

host = '127.0.0.1'

port class-attribute instance-attribute

port = 8791

data_dir class-attribute instance-attribute

data_dir = field(default_factory=lambda: default_data_dir())

database_url class-attribute instance-attribute

database_url = None

public_base_url class-attribute instance-attribute

public_base_url = 'http://127.0.0.1:8791'

token_pepper class-attribute instance-attribute

token_pepper = DEV_SECRET

admin_session_secret class-attribute instance-attribute

admin_session_secret = DEV_SECRET

admin_cidr_allowlist class-attribute instance-attribute

admin_cidr_allowlist = ('127.0.0.1/32', '::1/128')

cors_origins class-attribute instance-attribute

cors_origins = ()

max_upload_bytes class-attribute instance-attribute

max_upload_bytes = 25 * 1024 * 1024

max_sync_audio_seconds class-attribute instance-attribute

max_sync_audio_seconds = 120

max_async_audio_seconds class-attribute instance-attribute

max_async_audio_seconds = 900

max_audio_channels class-attribute instance-attribute

max_audio_channels = 2

max_sample_rate_hz class-attribute instance-attribute

max_sample_rate_hz = 48000

audio_probe_timeout_seconds class-attribute instance-attribute

audio_probe_timeout_seconds = 15

max_process_rss_bytes class-attribute instance-attribute

max_process_rss_bytes = 0

max_concurrent_transcriptions class-attribute instance-attribute

max_concurrent_transcriptions = 1

max_model_downloads class-attribute instance-attribute

max_model_downloads = 1

max_running_jobs class-attribute instance-attribute

max_running_jobs = 1

max_queued_jobs class-attribute instance-attribute

max_queued_jobs = 100

default_max_concurrent_async_jobs class-attribute instance-attribute

default_max_concurrent_async_jobs = 3

model_idle_ttl_seconds class-attribute instance-attribute

model_idle_ttl_seconds = 900

max_loaded_models class-attribute instance-attribute

max_loaded_models = 1

default_async_retention_hours class-attribute instance-attribute

default_async_retention_hours = 24

output_cache_enabled class-attribute instance-attribute

output_cache_enabled = False

request_timeout_seconds class-attribute instance-attribute

request_timeout_seconds = 900

async_job_runner class-attribute instance-attribute

async_job_runner = 'background'

cleanup_interval_seconds class-attribute instance-attribute

cleanup_interval_seconds = 3600

orphan_upload_retention_seconds class-attribute instance-attribute

orphan_upload_retention_seconds = 3600

audit_retention_days class-attribute instance-attribute

audit_retention_days = 90

preload_models class-attribute instance-attribute

preload_models = ()

warmup_audio_path class-attribute instance-attribute

warmup_audio_path = None

allow_trust_remote_code class-attribute instance-attribute

allow_trust_remote_code = False

resolved_database_url property

resolved_database_url

is_production property

is_production

__init__

__init__(
    environment="development",
    host="127.0.0.1",
    port=8791,
    data_dir=(lambda: default_data_dir())(),
    database_url=None,
    public_base_url="http://127.0.0.1:8791",
    token_pepper=DEV_SECRET,
    admin_session_secret=DEV_SECRET,
    admin_cidr_allowlist=("127.0.0.1/32", "::1/128"),
    cors_origins=(),
    max_upload_bytes=25 * 1024 * 1024,
    max_sync_audio_seconds=120,
    max_async_audio_seconds=900,
    max_audio_channels=2,
    max_sample_rate_hz=48000,
    audio_probe_timeout_seconds=15,
    max_process_rss_bytes=0,
    max_concurrent_transcriptions=1,
    max_model_downloads=1,
    max_running_jobs=1,
    max_queued_jobs=100,
    default_max_concurrent_async_jobs=3,
    model_idle_ttl_seconds=900,
    max_loaded_models=1,
    default_async_retention_hours=24,
    output_cache_enabled=False,
    request_timeout_seconds=900,
    async_job_runner="background",
    cleanup_interval_seconds=3600,
    orphan_upload_retention_seconds=3600,
    audit_retention_days=90,
    preload_models=(),
    warmup_audio_path=None,
    allow_trust_remote_code=False,
)

ensure_directories

ensure_directories()
Source code in src/audiotext/config.py
def ensure_directories(self) -> None:
    self.data_dir.mkdir(parents=True, exist_ok=True)
    (self.data_dir / "uploads").mkdir(parents=True, exist_ok=True)
    (self.data_dir / "results").mkdir(parents=True, exist_ok=True)
    (self.data_dir / "logs").mkdir(parents=True, exist_ok=True)

validate_for_startup

validate_for_startup()
Source code in src/audiotext/config.py
def validate_for_startup(self) -> None:
    if self.port < 1 or self.port > 65535:
        raise ValueError("port must be between 1 and 65535")
    if self.max_upload_bytes < 1:
        raise ValueError("max_upload_bytes must be positive")
    if self.audio_probe_timeout_seconds < 1:
        raise ValueError("audio_probe_timeout_seconds must be positive")
    if self.max_audio_channels < 1:
        raise ValueError("max_audio_channels must be positive")
    if self.max_sample_rate_hz < 1:
        raise ValueError("max_sample_rate_hz must be positive")
    if self.max_process_rss_bytes < 0:
        raise ValueError("max_process_rss_bytes must be zero or positive")
    if self.max_concurrent_transcriptions < 1:
        raise ValueError("max_concurrent_transcriptions must be positive")
    if self.max_model_downloads < 1:
        raise ValueError("max_model_downloads must be positive")
    if self.max_running_jobs < 1:
        raise ValueError("max_running_jobs must be positive")
    if self.max_queued_jobs < 1:
        raise ValueError("max_queued_jobs must be positive")
    if self.default_max_concurrent_async_jobs < 1:
        raise ValueError("default_max_concurrent_async_jobs must be positive")
    if self.max_loaded_models < 1:
        raise ValueError("max_loaded_models must be positive")
    if self.async_job_runner not in {"background", "external"}:
        raise ValueError("async_job_runner must be 'background' or 'external'")
    if self.cleanup_interval_seconds < 0:
        raise ValueError("cleanup_interval_seconds must be zero or positive")
    if self.orphan_upload_retention_seconds < 1:
        raise ValueError("orphan_upload_retention_seconds must be positive")
    if self.audit_retention_days < 1:
        raise ValueError("audit_retention_days must be positive")
    if self.is_production:
        missing = []
        if not self.token_pepper or self.token_pepper == DEV_SECRET:
            missing.append(f"{ENV_PREFIX}TOKEN_PEPPER")
        if not self.admin_session_secret or self.admin_session_secret == DEV_SECRET:
            missing.append(f"{ENV_PREFIX}ADMIN_SESSION_SECRET")
        if missing:
            raise ValueError("production requires configured secrets: " + ", ".join(missing))

safe_dict

safe_dict()
Source code in src/audiotext/config.py
def safe_dict(self) -> dict[str, object]:
    payload = asdict(self)
    payload["data_dir"] = str(self.data_dir)
    payload["database_url"] = redact_secret(self.resolved_database_url)
    payload["token_pepper"] = configured_state(self.token_pepper)
    payload["admin_session_secret"] = configured_state(self.admin_session_secret)
    return payload

load_config

load_config(*, env=None, config_file=None, overrides=None)
Source code in src/audiotext/config.py
def load_config(
    *,
    env: Mapping[str, str] | None = None,
    config_file: str | Path | None = None,
    overrides: Mapping[str, object] | None = None,
) -> AppConfig:
    source_env = env or os.environ
    file_values = _load_config_file(config_file or source_env.get(f"{ENV_PREFIX}CONFIG"))
    env_values = _load_env(source_env)
    merged = {**file_values, **env_values, **(overrides or {})}

    if "data_dir" in merged and not isinstance(merged["data_dir"], Path):
        merged["data_dir"] = Path(str(merged["data_dir"])).expanduser()
    if "admin_cidr_allowlist" in merged:
        merged["admin_cidr_allowlist"] = _tuple_value(merged["admin_cidr_allowlist"])
    if "cors_origins" in merged:
        merged["cors_origins"] = _tuple_value(merged["cors_origins"])
    if "preload_models" in merged:
        merged["preload_models"] = _tuple_value(merged["preload_models"])

    config = AppConfig(**merged)
    config.validate_for_startup()
    return config

apply_runtime_settings

apply_runtime_settings(config, settings)
Source code in src/audiotext/config.py
def apply_runtime_settings(config: AppConfig, settings: Mapping[str, object]) -> AppConfig:
    allowed = {
        "max_loaded_models",
        "model_idle_ttl_seconds",
        "max_upload_bytes",
        "max_model_downloads",
        "default_async_retention_hours",
        "max_running_jobs",
        "max_queued_jobs",
        "default_max_concurrent_async_jobs",
        "cleanup_interval_seconds",
        "orphan_upload_retention_seconds",
        "audit_retention_days",
        "output_cache_enabled",
    }
    updates = {key: value for key, value in settings.items() if key in allowed}
    if not updates:
        return config
    updated = replace(config, **updates)
    updated.validate_for_startup()
    return updated