From 661e2b1becc41a87e5614179db5178b453dfebaf Mon Sep 17 00:00:00 2001 From: John Lancaster <32917998+jsl12@users.noreply.github.com> Date: Sat, 1 Aug 2026 09:51:21 -0500 Subject: [PATCH] smoothed readme and startup --- .vscode/launch.json | 12 ++--- README.md | 78 ++++++++++++++++++++++++++++++--- src/transcription/config.py | 7 +-- src/transcription/db/session.py | 10 +++-- 4 files changed, 87 insertions(+), 20 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index cb732e3..6bf2bbe 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -8,14 +8,10 @@ "module": "debugpy", "args": [ "-m", - "uvicorn", - "transcription.app:create_app", - "--factory", - "--host", - // "127.0.0.1", - "0.0.0.0", - "--port", - "8080" + "transcription", + "--host", "127.0.0.1", + "--port", "9999", + "--database.driver", "sqlite" ], "justMyCode": true, "console": "integratedTerminal", diff --git a/README.md b/README.md index f9af9f4..29e2be2 100644 --- a/README.md +++ b/README.md @@ -22,18 +22,80 @@ uv sync ### 2) Configure environment -Create a `.env` file in the project root (minimum required setting shown): +Create a `.env` file in the project root with the required OpenRouter API key: ```env OPENROUTER_API_KEY=your_openrouter_api_key ``` -Optional settings (defaults shown): +Settings are read from CLI arguments first, then environment variables, then `.env`, then the defaults below. + +### Configuration Source Precedence + +When the same setting is provided in multiple places, the value is chosen in this order (highest priority first): + +1. CLI arguments (for example `--port 9999`) +2. Settings constructor arguments (used mainly in tests) +3. Environment variables +4. `.env` file values +5. Model defaults in code + +Practical examples: + +- `--port 9999` overrides both `PORT=8000` in the shell and `PORT=7000` in `.env`. +- `DATABASE__PATH=prod.db` in the shell overrides `DATABASE__PATH=dev.db` in `.env`. + +#### Server and runtime + +| Environment variable | Default | Description | +| --- | --- | --- | +| `HOST` | `0.0.0.0` | Address on which the server listens. | +| `PORT` | `8000` | Server port. | +| `LOG_LEVEL` | `info` | Uvicorn and application log level. | +| `RELOAD` | `false` | Restart the development server when source files change. | +| `ENVIRONMENT` | `development` | Runtime environment: `development`, `test`, or `production`. | + +#### Provider + +| Environment variable | Default | Description | +| --- | --- | --- | +| `PROVIDER` | `openrouter` | Transcription provider. | +| `OPENROUTER_API_KEY` | Required | OpenRouter API key. | +| `PROVIDER_MODEL` | Provider default | Optional model override. | +| `OPENROUTER_HTTP_REFERER` | Unset | Optional OpenRouter attribution URL. | +| `OPENROUTER_APP_TITLE` | Unset | Optional OpenRouter attribution title. | + +#### Database and files + +Use nested env vars for database settings (recommended): ```env -DATABASE_URL=sqlite:///./transcription.db +DATABASE__DRIVER=sqlite +DATABASE__PATH=app.db +# BOOTSTRAP_SCHEMA_ON_STARTUP=true +SQLITE_CHECK_SAME_THREAD=false UPLOAD_DIR=./uploads PROMPT_DIR=./prompts +``` + +For PostgreSQL: + +```env +DATABASE__DRIVER=postgres +DATABASE__HOST=localhost +DATABASE__PORT=5432 +DATABASE__DATABASE=transcription +DATABASE__USER=postgres +DATABASE__PASSWORD=change-me +``` + +This uses Pydantic nested settings (`env_nested_delimiter='__'`) and avoids JSON blobs in `.env`. A top-level `DATABASE={...}` JSON value is still supported as a fallback, and nested keys such as `DATABASE__PATH` take precedence over conflicting JSON keys. + +`BOOTSTRAP_SCHEMA_ON_STARTUP` creates missing tables when the app starts. When unset, it is enabled in `development` and `test`, and disabled in `production`; set it explicitly to override that policy. `SQLITE_CHECK_SAME_THREAD` defaults to `false`. + +#### Worker + +```env WORKER_MAX_RETRIES=0 WORKER_RETRY_BACKOFF_SECONDS=0 WORKER_PROVIDER_TIMEOUT_SECONDS=20 @@ -45,13 +107,17 @@ WORKER_FAIL_ON_FINISH_REASON_LENGTH=false ### 3) Run the app ```bash -uv run uvicorn transcription.app:create_app --factory --reload +uv run python -m transcription --port 9999 --reload --database.driver sqlite --bootstrap-schema-on-startup ``` +This starts the development server with SQLite, creates missing tables, and enables automatic reload. Run `uv run python -m transcription --help` for all CLI options; CLI names use kebab case and nested database options use dot notation, such as `--database.path ./data/transcription.db`. + ### 4) Open in browser -- GUI: [http://[IP_ADDRESS]:8000/ui](http://[IP_ADDRESS]:8000/ui) -- Health check: [http://[IP_ADDRESS]:8000/healthz](http://[IP_ADDRESS]:8000/healthz) +- GUI: [http://localhost:9999/ui](http://localhost:9999/ui) +- Health check: [http://localhost:9999/healthz](http://localhost:9999/healthz) + +Replace `localhost` with the server's hostname or IP address when connecting from another machine. ## How to navigate the GUI diff --git a/src/transcription/config.py b/src/transcription/config.py index 44e597f..51bf399 100644 --- a/src/transcription/config.py +++ b/src/transcription/config.py @@ -52,6 +52,7 @@ class Settings(BaseSettings): env_file=".env", env_file_encoding="utf-8", extra="ignore", + env_nested_delimiter="__", cli_implicit_flags=True, cli_kebab_case=True, ) @@ -92,7 +93,7 @@ class Settings(BaseSettings): @property def should_bootstrap_schema(self) -> bool: """Return whether startup should auto-create schema for this environment.""" - if self.bootstrap_schema_on_startup is not None: + if "bootstrap_schema_on_startup" in self.model_fields_set: return self.bootstrap_schema_on_startup return self.environment in {"development", "test"} @@ -100,13 +101,13 @@ class Settings(BaseSettings): @cache def get_settings(**kwargs: Any) -> Settings: """Load cached settings without reading process CLI arguments.""" - return Settings(_cli_parse_args=False, **kwargs) + return Settings(_cli_parse_args=False, **kwargs) # pyright: ignore[reportCallIssue] def parse_cli_settings(args: Sequence[str] | None = None) -> Settings: """Load settings with CLI arguments at the executable boundary.""" cli_args = True if args is None else list(args) - return Settings(_cli_parse_args=cli_args) + return Settings(_cli_parse_args=cli_args) # pyright: ignore[reportCallIssue] LOGGING_CONFIG: dict[str, Any] = { diff --git a/src/transcription/db/session.py b/src/transcription/db/session.py index 140a0c5..366ddd2 100644 --- a/src/transcription/db/session.py +++ b/src/transcription/db/session.py @@ -31,8 +31,9 @@ def resolve_session_factory( *, settings: Settings | None = None, ) -> SessionFactory: - active_settings = settings or get_settings() - return get_session_factory(database_url or get_database_url(active_settings)) + if database_url is not None: + return get_session_factory(database_url) + return get_session_factory(get_database_url(settings or get_settings())) type SessionFactoryDep = Annotated[SessionFactory, Depends(resolve_session_factory)] @@ -92,4 +93,7 @@ async def transaction_scope( yield owned_session -type TransactionScopeDep = Annotated[AsyncSessionTransaction, Depends(transaction_scope)] +type TransactionScopeDep = Annotated[ + AsyncSession | AsyncSessionTransaction, + Depends(transaction_scope), +]