Recently Written · git

subplz-web

git clone https://github.com/equwal/subplz-web

Log | Files | Refs


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)