backend/settings.py (6157 bytes)
1 """Configuration. Every scale-out seam is an env var with a localhost-friendly default.""" 2 3 import shutil 4 from pathlib import Path 5 from typing import Literal 6 7 from pydantic_settings import BaseSettings, SettingsConfigDict 8 9 ROOT = Path(__file__).resolve().parent.parent 10 11 12 def _find_subplz() -> str: 13 """Locate the alignment backend on PATH. 14 15 subplz is an ordinary dependency of this project, installed into whatever 16 environment is running it - not a sibling checkout. Falling back to the bare 17 name keeps the error useful when it is genuinely missing. 18 """ 19 return shutil.which("subplz") or "subplz" 20 21 22 class Settings(BaseSettings): 23 model_config = SettingsConfigDict(env_prefix="SUBPLZ_WEB_", env_file=".env", extra="ignore") 24 25 # --- paths ------------------------------------------------------------- 26 data_dir: Path = ROOT / "data" 27 # Override with SUBPLZ_WEB_SUBPLZ_BIN to point at a specific build. 28 subplz_bin: Path | str = _find_subplz() 29 30 # --- alignment --------------------------------------------------------- 31 # Which backend does the aligning. See aligner.py to add another. 32 aligner: str = "subplz" 33 # "tiny" is what upstream recommends for audiobooks: the transcript only has 34 # to be good enough to align against text we already have. 35 model: str = "tiny" 36 device: Literal["cpu", "cuda"] = "cpu" 37 # 0 = let the runner pick from CPU count. 38 threads: int = 0 39 # Wall-clock ceiling per job so a wedged run cannot hold a worker forever. 40 job_timeout_seconds: int = 6 * 60 * 60 41 42 # --- scale-out seams --------------------------------------------------- 43 # "memory" runs jobs in a thread in this process (localhost). 44 # "redis" hands them to external workers (public deployment). 45 queue_backend: Literal["memory", "redis"] = "memory" 46 redis_url: str = "redis://localhost:6379/0" 47 # How many jobs this process will run at once when queue_backend="memory". 48 max_concurrent_jobs: int = 1 49 50 # "local" writes under data_dir. "s3" writes to a bucket. 51 storage_backend: Literal["local", "s3"] = "local" 52 s3_bucket: str = "" 53 s3_prefix: str = "jobs/" 54 # Presigned-URL lifetime for s3 downloads. 55 download_url_ttl_seconds: int = 3600 56 57 # How long the server keeps a visitor's files. See retention.py. 58 input_retention_hours: int = 24 59 artifact_retention_days: int = 7 60 61 # SQLite locally; set to a postgresql+psycopg:// URL in production. 62 database_url: str = "" 63 64 # --- billing ----------------------------------------------------------- 65 # The public offer: free in the visitor's browser; a conversion on this 66 # server's hardware takes a credit. See billing.py. 67 # True when fast conversion on this server's hardware is on offer. While it 68 # is false the site sells nothing, whatever billing_enabled says: credits 69 # that buy nothing must not be for sale. 70 cloud_enabled: bool = False 71 # Where customers write to: shown on the terms page and in the footer. The 72 # payment processor wants one on the site before it lets a shop go live. 73 contact_email: str = "" 74 # Off on localhost so nothing blocks you; flip on for the public release. 75 billing_enabled: bool = False 76 77 # --- accounts & payments ----------------------------------------------- 78 # What customers see on the Stripe page and in the sign-in email. 79 site_name: str = "SubRead" 80 # The origin users reach this on. Sign-in links and Stripe's return URLs 81 # are built from it, so it must be right in production. 82 public_base_url: str = "http://127.0.0.1:8420" 83 # Send the identity cookie over HTTPS only. On for any public deployment. 84 cookie_secure: bool = False 85 86 # Stripe. Leave empty and checkout reports itself unavailable rather than 87 # failing half way through a purchase. 88 stripe_secret_key: str = "" 89 stripe_webhook_secret: str = "" 90 91 # Sign-in links go out over plain SMTP, so any provider works. With no 92 # host set, sign-in is simply unavailable (see dev_login_links below). 93 smtp_host: str = "" 94 smtp_port: int = 587 95 smtp_user: str = "" 96 smtp_password: str = "" 97 smtp_from: str = "SubRead <login@subread.space>" 98 smtp_starttls: bool = True 99 login_link_minutes: int = 30 100 # Hand the sign-in link back in the API response instead of mailing it. 101 # For development ONLY: it signs anyone in as any address. Never inferred 102 # from other settings - a public server with billing off is still public. 103 dev_login_links: bool = False 104 105 # --- match check ------------------------------------------------------- 106 # Transcribe a few short samples on upload and score them against the book, 107 # so a mismatched pair fails in seconds instead of after a full run. 108 match_check: bool = True 109 # Clean the book text before alignment to raise the match score. 110 text_prep: bool = True 111 112 # --- video ------------------------------------------------------------- 113 # Render a YouTube-ready MP4 (cover image + audio + soft subtitle track). 114 render_video: bool = True 115 video_width: int = 1920 116 video_height: int = 1080 117 # 1 fps is the minimum YouTube accepts and all a still image needs. 118 video_fps: int = 1 119 # "auto" picks the best H.264 encoder this ffmpeg build actually has. 120 # Force one with libx264 / h264_nvenc / h264_amf / h264_qsv / libopenh264. 121 video_encoder: str = "auto" 122 video_crf: int = 28 123 124 # --- uploads ----------------------------------------------------------- 125 max_upload_bytes: int = 2 * 1024 * 1024 * 1024 # 2 GiB 126 127 @property 128 def payments_configured(self) -> bool: 129 return bool(self.stripe_secret_key) 130 131 @property 132 def email_configured(self) -> bool: 133 return bool(self.smtp_host) 134 135 @property 136 def sign_in_available(self) -> bool: 137 return self.email_configured or self.dev_login_links 138 139 @property 140 def resolved_database_url(self) -> str: 141 if self.database_url: 142 return self.database_url 143 return f"sqlite:///{(self.data_dir / 'subplz.db').as_posix()}" 144 145 146 settings = Settings() 147 settings.data_dir.mkdir(parents=True, exist_ok=True)