From c5c4a6ac540f370591f58836276e642dd23d8ef3 Mon Sep 17 00:00:00 2001 From: amide-init Date: Fri, 9 Oct 2026 05:58:09 -0700 Subject: [PATCH 1/4] chore: remove scripts/setup-mac.sh The setup steps work the same on every platform (pnpm install, copy .env, prisma generate), so a separate macOS-only script to maintain isn't worth it. The docs now have one setup path, starting from git clone, which the script's section used to carry. The useful part of the script, catching an FFmpeg build without libass, is still covered by the FFMPEG_PATH note in server/.env.example. Co-Authored-By: Claude Opus 5.5 --- CONTRIBUTING.md | 8 +- README.md | 16 +--- docs/community.md | 4 +- docs/guide/getting-started.md | 22 +---- scripts/setup-mac.sh | 150 ---------------------------------- 5 files changed, 6 insertions(+), 194 deletions(-) delete mode 100755 scripts/setup-mac.sh diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 06a390b..1f4d3e7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -7,9 +7,6 @@ issues for bugs and ideas. ## Running it locally -On macOS, `./scripts/setup-mac.sh` handles all of this for you (Node/pnpm/ -Bun/FFmpeg via Homebrew, `pnpm install`, `prisma generate`, and `.env`) -- -see the README's "macOS quick start". Otherwise, manually: ```bash pnpm install # installs deps for client + server @@ -27,9 +24,8 @@ does. You'll also need [FFmpeg](https://ffmpeg.org/download.html) and [Bun](https://bun.sh) (the backend's runtime) on your machine. If burning in captions fails with `No such filter: 'subtitles'`, your FFmpeg build -doesn't include libass — see the `FFMPEG_PATH` note in `.env.example` (or -just use `scripts/setup-mac.sh`, which installs and points at a build -that has it). +doesn't include libass — see the `FFMPEG_PATH` note in `.env.example` +(on macOS, Homebrew's `ffmpeg-full` has it). ## Before submitting a PR diff --git a/README.md b/README.md index 910ebc8..243613a 100644 --- a/README.md +++ b/README.md @@ -129,25 +129,11 @@ Requires [FFmpeg](https://ffmpeg.org/download.html) (with `ffprobe`) and OpenAI API key (used server-side only, for transcription and the AI buttons). -### macOS quick start - -[`scripts/setup-mac.sh`](./scripts/setup-mac.sh) installs Node/pnpm/Bun/ -FFmpeg (with subtitle burn-in support) via Homebrew if you don't already -have them, installs dependencies for the whole workspace, and walks you -through `.env`: +### Setup ```bash git clone https://github.com/amide-init/transcriptcut.git cd transcriptcut -./scripts/setup-mac.sh -``` - -Then `pnpm run dev` from the repo root. Safe to re-run -- it only -installs what's missing and never overwrites an existing `.env`. - -### Manual setup (any platform) - -```bash pnpm install # installs deps for client + server cd server cp .env.example .env # then fill in OPENAI_API_KEY diff --git a/docs/community.md b/docs/community.md index 19c7b16..390e6b5 100644 --- a/docs/community.md +++ b/docs/community.md @@ -26,8 +26,8 @@ OS/Node/FFmpeg versions if it's a rendering or transcription issue. and [`claude.md`](https://github.com/amide-init/transcriptcut/blob/main/claude.md) (the full product spec — read it before proposing anything that changes how the app is structured). -2. Set up locally with `./scripts/setup-mac.sh` (macOS) or the manual - steps in [Getting Started](/guide/getting-started). +2. Set up locally with the steps in + [Getting Started](/guide/getting-started). 3. Before opening a PR, all of these must pass (CI runs the same three commands on every PR): diff --git a/docs/guide/getting-started.md b/docs/guide/getting-started.md index fe98aea..8429db7 100644 --- a/docs/guide/getting-started.md +++ b/docs/guide/getting-started.md @@ -14,31 +14,11 @@ speakers, chapters, show notes, highlights), never for storage. `PATH` (used for audio extraction, cuts, audio cleanup and export) - An OpenAI API key (used server-side only) -## macOS quick start - -[`scripts/setup-mac.sh`](https://github.com/amide-init/transcriptcut/blob/main/scripts/setup-mac.sh) -installs Node/pnpm/Bun/FFmpeg (with subtitle burn-in support) via -Homebrew if you don't already have them, installs dependencies for the -whole workspace, and walks you through `.env`: +## Setup ```bash git clone https://github.com/amide-init/transcriptcut.git cd transcriptcut -./scripts/setup-mac.sh -``` - -Then, from the repo root: - -```bash -pnpm run dev -``` - -Safe to re-run — it only installs what's missing and never overwrites an -existing `.env`. - -## Manual setup (any platform) - -```bash pnpm install # installs deps for client + server cd server cp .env.example .env # then fill in OPENAI_API_KEY diff --git a/scripts/setup-mac.sh b/scripts/setup-mac.sh deleted file mode 100755 index 8a7b66a..0000000 --- a/scripts/setup-mac.sh +++ /dev/null @@ -1,150 +0,0 @@ -#!/usr/bin/env bash -# -# One-shot setup for running this project on macOS: installs Node/pnpm/ -# Bun/ffmpeg-full via Homebrew (Bun via its own official installer, see -# below) if missing, installs JS deps for the whole workspace, generates -# the server's Prisma client, and creates server/.env (with FFMPEG_PATH -# pre-filled and an interactive OPENAI_API_KEY prompt) if one doesn't -# already exist. -# -# Safe to re-run -- every step checks before it acts, and an existing -# server/.env is never overwritten. - -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" -SERVER_DIR="$REPO_ROOT/server" - -step() { printf '\n==> %s\n' "$1"; } - -# Replaces the (whole-line) $2 with $3 in file $1, as an exact literal -# line match via awk -- not sed (would need regex escaping for the `/` and -# `"` in a path/key) and not bash's ${var/pattern/replacement} (tried -# first, but macOS ships bash 3.2 -- Apple has never upgraded it, over -# GPLv3 licensing -- and its parameter-substitution pattern matching has -# real quoting/anchoring quirks: quoting $pattern/$replacement inserted -# literal quote characters into the result, and unquoting that broke -# matching for any pattern starting with '#', both verified reproducing -# on this exact bash 3.2 and not on bash 4+). awk's -v passes strings -# through literally, with no glob/regex reinterpretation either way. -set_env_var() { - local file="$1" pattern="$2" replacement="$3" - awk -v pat="$pattern" -v rep="$replacement" ' - $0 == pat { print rep; next } - { print } - ' "$file" > "$file.tmp" && mv "$file.tmp" "$file" -} - -step "Checking you're on macOS" -if [[ "$(uname)" != "Darwin" ]]; then - echo "This script is macOS-only. See CONTRIBUTING.md for the manual setup steps on other platforms." - exit 1 -fi - -step "Checking for Homebrew" -if ! command -v brew &>/dev/null; then - cat <<'EOF' -Homebrew isn't installed. Install it first (see https://brew.sh), then re-run this script: - - /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" -EOF - exit 1 -fi -echo "Found Homebrew." - -step "Checking for Node 20+" -if command -v node &>/dev/null; then - node_major="$(node -v | sed 's/^v//' | cut -d. -f1)" - if [ "$node_major" -lt 20 ]; then - echo "Found Node $(node -v), but 20+ is required. Upgrade it (e.g. 'brew upgrade node') and re-run this script." - exit 1 - fi - echo "Found Node $(node -v)." -else - echo "Installing Node via Homebrew..." - brew install node -fi - -step "Checking for pnpm" -if command -v pnpm &>/dev/null; then - echo "Found pnpm $(pnpm -v)." -else - echo "Installing pnpm via Homebrew..." - brew install pnpm -fi - -step "Checking for Bun" -if command -v bun &>/dev/null; then - echo "Found Bun $(bun -v)." -else - # Bun's own installer, not Homebrew: the Homebrew formula requires - # compiling a native dependency (better-sqlite3, transitively) against - # Xcode Command Line Tools, and fails on a CLT version that's gone - # stale relative to the current macOS/Xcode release -- a common state - # on a machine that hasn't run a manual CLT update in a while. Bun's - # installer just downloads a prebuilt binary, sidestepping that - # entirely (see https://bun.sh). - echo "Installing Bun via its official installer..." - curl -fsSL https://bun.sh/install | bash - export PATH="$HOME/.bun/bin:$PATH" - echo "Installed Bun $(bun -v). Open a new terminal (or re-source your shell rc) to get 'bun' on PATH permanently." -fi - -step "Checking ffmpeg has subtitle burn-in (libass) support" -ffmpeg_path_override="" -if command -v ffmpeg &>/dev/null && ffmpeg -filters 2>/dev/null | grep -qw subtitles; then - echo "Default ffmpeg already supports subtitles -- nothing to do." -else - echo "Default ffmpeg is missing or lacks libass support (needed for 'Burn in captions')." - echo "Installing ffmpeg-full via Homebrew..." - brew install ffmpeg-full - # ffmpeg-full is keg-only (its ffmpeg/ffprobe binaries conflict with the - # plain ffmpeg formula's), so it's never on PATH -- the app needs the - # explicit path via FFMPEG_PATH, filled into .env below. brew --prefix - # resolves correctly on both Apple Silicon (/opt/homebrew) and Intel - # (/usr/local), unlike a hardcoded path. - ffmpeg_path_override="$(brew --prefix ffmpeg-full)/bin/ffmpeg" -fi - -step "Installing JS dependencies (client + server workspace)" -(cd "$REPO_ROOT" && pnpm install) - -step "Generating the Prisma client" -(cd "$SERVER_DIR" && bunx prisma generate) - -step "Setting up server/.env" -env_file="$SERVER_DIR/.env" -if [ -f "$env_file" ]; then - echo "server/.env already exists -- leaving it as-is." -else - cp "$SERVER_DIR/.env.example" "$env_file" - echo "Created server/.env from .env.example." - - if [ -n "$ffmpeg_path_override" ]; then - set_env_var "$env_file" \ - '# FFMPEG_PATH="/opt/homebrew/opt/ffmpeg-full/bin/ffmpeg"' \ - "FFMPEG_PATH=\"$ffmpeg_path_override\"" - echo "Set FFMPEG_PATH to the Homebrew ffmpeg-full binary (fixes caption burn-in)." - fi - - openai_key_input="" - read -rsp "Paste your OpenAI API key (used server-side only; Enter to skip and edit server/.env by hand later): " openai_key_input || true - echo - if [ -n "$openai_key_input" ]; then - set_env_var "$env_file" "OPENAI_API_KEY=sk-..." "OPENAI_API_KEY=$openai_key_input" - echo "Set OPENAI_API_KEY in server/.env." - else - echo "Skipped -- transcription and AI features won't work until you set OPENAI_API_KEY in server/.env." - fi -fi - -step "Done" -cat < Date: Fri, 9 Oct 2026 06:19:19 -0700 Subject: [PATCH 2/4] build: add a Docker image and compose file Running the app meant installing Node, pnpm, Bun and an FFmpeg build with libass by hand. The image does that once: a Node stage builds the client and a production-only server, and an oven/bun runtime serves both from one port with data under a /data volume. Startup migrations turn an empty volume into a working database, so no Prisma CLI is needed. FFmpeg is the static 9.0.2 build (libass included, and the version the xfade workarounds in plan.ts were measured on), not Debian's 5.1. Caption and card fonts are Liberation + DejaVu stand-ins rather than Microsoft's core fonts, which would be a build-time download with licensing caveats. compose publishes on 127.0.0.1 only, since the app has no login. Also drops better-sqlite3 from onlyBuiltDependencies: it's only an optional peer of the Prisma CLI (the app uses adapter-libsql), and its native build fails noisily in a container without Python. Co-Authored-By: Claude Opus 5.5 --- .dockerignore | 13 +++++++++++++ Dockerfile | 53 +++++++++++++++++++++++++++++++++++++++++++++++++++ compose.yaml | 23 ++++++++++++++++++++++ package.json | 1 - 4 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 compose.yaml diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..505834e --- /dev/null +++ b/.dockerignore @@ -0,0 +1,13 @@ +# Keep local data, keys and build output out of the image's build context. +**/node_modules +**/dist +**/data +**/.env* +!**/.env.example +**/*.tsbuildinfo +server/src/generated +.git +.github +.claude +docs +client/.agents diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..dbbe1f7 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,53 @@ +# Runs the app as one container: the Bun server serves both the built +# client and /api/* on port 3001, with all project data under /data. +# See compose.yaml for how to run it, and claude.md section 35. + +# --- build: install the workspace, generate Prisma, build the client ---- +FROM node:22-bookworm-slim AS build +RUN corepack enable && corepack prepare pnpm@10.29.2 --activate +WORKDIR /src + +# Manifests first, so the dependency layer is cached across source edits. +COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ +COPY client/package.json client/ +COPY server/package.json server/ +RUN pnpm install --frozen-lockfile + +COPY client client +COPY server server +RUN pnpm --filter server db:generate \ + && pnpm --filter client build \ + && pnpm --filter server deploy --prod --legacy /out/server + +# --- runtime ------------------------------------------------------------ +FROM oven/bun:1.4.2-debian + +# Fonts for burned-in captions and title cards: Liberation stands in for +# Arial/Helvetica/Courier New (metric-compatible, aliased by fontconfig), +# DejaVu is the fallback for the rest of CAPTION_FONTS. +RUN apt-get update \ + && apt-get install -y --no-install-recommends fontconfig fonts-liberation2 fonts-dejavu-core \ + && rm -rf /var/lib/apt/lists/* + +# Static ffmpeg/ffprobe with libass, pinned to the version the xfade +# workarounds in lib/ffmpeg/plan.ts were measured on. +COPY --from=mwader/static-ffmpeg:9.0.2 /ffmpeg /ffprobe /usr/local/bin/ + +WORKDIR /app +COPY --from=build /out/server ./ +# Gitignored, so `pnpm deploy` leaves it out. +COPY --from=build /src/server/src/generated ./src/generated +COPY --from=build /src/client/dist ./client-dist + +ENV NODE_ENV=production \ + PORT=3001 \ + DATA_DIR=/data \ + DATABASE_URL=file:/data/app.db \ + CLIENT_DIST_DIR=/app/client-dist + +RUN mkdir -p /data && chown bun:bun /data +USER bun +VOLUME /data +EXPOSE 3001 + +CMD ["bun", "src/index.ts"] diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..8c3ce8d --- /dev/null +++ b/compose.yaml @@ -0,0 +1,23 @@ +# docker compose up --build, then open http://localhost:3001 +name: transcriptcut + +services: + app: + build: . + # Bound to this machine only: there's no login (claude.md section 18), + # so don't publish it to a shared network without adding auth first. + ports: + - "127.0.0.1:3001:3001" + volumes: + - transcriptcut-data:/data + # Optional: OPENAI_API_KEY (and MAX_UPLOAD_MB) from a .env next to this + # file. Without one, the app asks for the key on first launch. + env_file: + - path: .env + required: false + restart: unless-stopped + +volumes: + transcriptcut-data: + # Fixed name, so it doesn't depend on the folder the repo is cloned into. + name: transcriptcut-data diff --git a/package.json b/package.json index 8567a06..b7fb75b 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,6 @@ "pnpm": { "onlyBuiltDependencies": [ "@prisma/engines", - "better-sqlite3", "prisma", "protobufjs", "unrs-resolver" From 77f1a8816db86570a03167db7e8be2928f400287 Mon Sep 17 00:00:00 2001 From: amide-init Date: Fri, 9 Oct 2026 06:19:19 -0700 Subject: [PATCH 3/4] docs: lead with Docker for running the app Docker is now the documented way to run the app; the pnpm setup stays as the development setup for contributors. Covers the API key (setup screen or .env), where data lives, how to update, the localhost-only port and why caption fonts can differ from a Mac. Co-Authored-By: Claude Opus 5.5 --- CONTRIBUTING.md | 3 +++ README.md | 35 +++++++++++++++++++++++++----- claude.md | 23 ++++++++++++++++++-- docs/guide/getting-started.md | 41 ++++++++++++++++++++++++++++++----- docs/guide/security.md | 5 +++++ 5 files changed, 94 insertions(+), 13 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1f4d3e7..cefecda 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -7,6 +7,9 @@ issues for bugs and ideas. ## Running it locally +Just want to use the app? `docker compose up --build` runs it without +any of the tools below; see the README. For working on the code: + ```bash pnpm install # installs deps for client + server diff --git a/README.md b/README.md index 243613a..d318054 100644 --- a/README.md +++ b/README.md @@ -124,12 +124,37 @@ pnpm run dev ## Running the app -Requires [FFmpeg](https://ffmpeg.org/download.html) (with `ffprobe`) and -[Bun](https://bun.sh) (the backend's runtime) on your machine, plus an -OpenAI API key (used server-side only, for transcription and the AI -buttons). +The easiest way to run it is with [Docker](https://docs.docker.com/get-docker/). +The image includes Bun, FFmpeg (with subtitle burn-in support) and the +fonts captions need. You only bring an OpenAI API key, used server-side +for transcription and the AI buttons. -### Setup +### With Docker + +```bash +git clone https://github.com/amide-init/transcriptcut.git +cd transcriptcut +docker compose up --build +``` + +Open [http://localhost:3001](http://localhost:3001). On first launch the +app asks for your OpenAI API key. To skip that, create a `.env` file next to +`compose.yaml` containing `OPENAI_API_KEY=sk-...` before starting. + +Projects, uploads and renders live in the `transcriptcut-data` Docker +volume, so they survive restarts and rebuilds. To update, run +`git pull && docker compose up --build`. + +The app is published on `127.0.0.1` only, since it has no login (see +`claude.md` section 18). Caption fonts use free Liberation and DejaVu equivalents of Arial, +Helvetica, Verdana and the rest, so burned-in captions can look slightly +different from the same export on a Mac. + +### Development setup + +For working on the code, with hot reload. Requires +[FFmpeg](https://ffmpeg.org/download.html) (with `ffprobe`) and +[Bun](https://bun.sh) on your machine: ```bash git clone https://github.com/amide-init/transcriptcut.git diff --git a/claude.md b/claude.md index 80b353c..b931836 100644 --- a/claude.md +++ b/claude.md @@ -1476,8 +1476,8 @@ with the same care as the code itself. # 35. Web Only (no native app) -The app ships as the web setup only: `pnpm run dev` locally, or the Bun -server serving the built client in production. A Tauri-wrapped macOS +The app ships as the web setup only: `docker compose up` to run it, or +`pnpm run dev` to develop it. A Tauri-wrapped macOS `.app` (and its CI release workflow) existed earlier and was deliberately removed to keep the project to one supported setup; don't re-add native packaging speculatively. @@ -1492,3 +1492,22 @@ any deployment: `client/src/routes/SetupRoute.tsx`, stored in `DATA_DIR/settings.json` (`server/src/lib/settings.ts`), which is checked before falling back to `server/.env`'s `OPENAI_API_KEY`. + +## Docker + +The run path is one container (`Dockerfile`, `compose.yaml` at the repo +root): a Node build stage installs the workspace, runs `prisma generate`, +builds the client and `pnpm deploy`s a production-only server; the +runtime is `oven/bun` running `src/index.ts`, serving the client from +`CLIENT_DIST_DIR` and keeping everything under `DATA_DIR=/data` (a named +volume). A fresh volume works because startup migrations build the +schema. Choices worth knowing before changing them: + +* FFmpeg is the pinned static build `mwader/static-ffmpeg:9.0.2`, not + Debian's (5.1): it has libass, and 9.0.2 is the version the xfade + workarounds in section 12 were measured on. +* Caption and card fonts are Liberation + DejaVu, not Microsoft's core + fonts (a build-time SourceForge download with licensing caveats), so + burned-in text can differ slightly from a Mac. +* compose publishes on `127.0.0.1` only, since there's no auth + (section 18). diff --git a/docs/guide/getting-started.md b/docs/guide/getting-started.md index 8429db7..0e6654c 100644 --- a/docs/guide/getting-started.md +++ b/docs/guide/getting-started.md @@ -1,11 +1,40 @@ # Getting Started -transcriptcut is local-first: everything runs on your own machine with -`pnpm install && pnpm run dev`. The only external dependency is an -OpenAI API key, used for transcription and the AI buttons (filler words, -speakers, chapters, show notes, highlights), never for storage. +transcriptcut is local-first: everything runs on your own machine. The +only external dependency is an OpenAI API key, used for transcription and +the AI buttons (filler words, speakers, chapters, show notes, +highlights), never for storage. -## Requirements +## Run with Docker + +The easiest way to run it. All you need is +[Docker](https://docs.docker.com/get-docker/); the image includes Bun, +FFmpeg (with subtitle burn-in support) and the fonts captions need. + +```bash +git clone https://github.com/amide-init/transcriptcut.git +cd transcriptcut +docker compose up --build +``` + +Open `http://localhost:3001`. On first launch the app asks +for your OpenAI API key. To skip that, create a `.env` file next to +`compose.yaml` containing `OPENAI_API_KEY=sk-...` before starting. + +Projects, uploads and renders live in the `transcriptcut-data` Docker +volume, so they survive restarts and rebuilds. To update, run +`git pull && docker compose up --build`. + +The app is published on `127.0.0.1` only, since it has no login (see +[Security & Self-Hosting](/guide/security)). Caption fonts use free Liberation and DejaVu equivalents of Arial, +Helvetica, Verdana and the rest, so burned-in captions can look slightly +different from the same export on a Mac. + +## Development setup + +For working on the code, with hot reload. + +### Requirements - [Node.js](https://nodejs.org/) 20+ - [pnpm](https://pnpm.io/) 10+ @@ -14,7 +43,7 @@ speakers, chapters, show notes, highlights), never for storage. `PATH` (used for audio extraction, cuts, audio cleanup and export) - An OpenAI API key (used server-side only) -## Setup +### Setup ```bash git clone https://github.com/amide-init/transcriptcut.git diff --git a/docs/guide/security.md b/docs/guide/security.md index b49a60c..2809305 100644 --- a/docs/guide/security.md +++ b/docs/guide/security.md @@ -13,6 +13,11 @@ create projects, upload video, trigger renders, read/delete anything. network without adding real authentication first.** Running it on `localhost` on your own machine is the intended v1 use case. +The Docker setup follows this: `compose.yaml` publishes the app on +`127.0.0.1:3001`, so other machines on your network can't reach it. +Changing that to `3001:3001` (or `0.0.0.0`) exposes it to everyone on +the network, so only do that behind real authentication. + ## What the app validates, and why it matters more locally Even without accounts, this is an app that runs FFmpeg and touches the From a1b75e65981325b120a3f80b88581b2c47fef52f Mon Sep 17 00:00:00 2001 From: amide-init Date: Fri, 9 Oct 2026 06:25:44 -0700 Subject: [PATCH 4/4] chore: move the env file to the repo root With Docker, compose reads .env from the repo root while the dev server read server/.env, so running both meant keeping two files in sync. Now there's one root .env for both. The dev server loads it with bun --env-file=../.env, since Bun's own loading only looks in the working directory (server/). prisma7.config.ts loads it too, which also fixes the Prisma CLI: it never read server/.env, so migrate commands failed for lack of DATABASE_URL. compose now pins the container's paths, port and FFmpeg binaries over the env file, because dev values in it (relative paths, a Homebrew FFMPEG_PATH) don't exist inside the container. In .env.example only OPENAI_API_KEY is left uncommented; the rest match the code's defaults. Co-Authored-By: Claude Opus 5.5 --- .env.example | 43 +++++++++++++++++++++++++++++++++++ CONTRIBUTING.md | 12 ++++------ README.md | 17 +++++++------- claude.md | 9 +++++++- compose.yaml | 14 ++++++++++-- docs/guide/getting-started.md | 17 +++++++------- docs/guide/podcasting.md | 2 +- server/.env.example | 34 --------------------------- server/package.json | 4 ++-- server/prisma7.config.ts | 10 ++++++-- server/src/lib/settings.ts | 2 +- server/src/routes/settings.ts | 2 +- 12 files changed, 97 insertions(+), 69 deletions(-) create mode 100644 .env.example delete mode 100644 server/.env.example diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..5fcfdfb --- /dev/null +++ b/.env.example @@ -0,0 +1,43 @@ +# Copy this file to .env (in the repo root, next to this file) and fill in +# the values below. Never commit your actual .env -- it's gitignored on +# purpose. +# +# One file for both setups: +# - `pnpm run dev`: the server loads it with `bun --env-file=../.env` +# (server/package.json), and the Prisma CLI through prisma7.config.ts. +# - `docker compose up`: passed to the container by compose.yaml. Only +# OPENAI_API_KEY and MAX_UPLOAD_MB apply there; the paths and port below +# are fixed inside the container. + +# Required for `pnpm run dev`. Used server-side only (transcription via +# Whisper and the AI buttons) -- never exposed to the browser. Optional +# with Docker, where the app asks for it on first launch instead. +OPENAI_API_KEY=sk-... + +# Optional. Largest accepted video upload, in MB (default 10240 = 10GB). +# Uploads stream straight to disk, so this is about disk space, not memory. +# MAX_UPLOAD_MB=10240 + +# --- Development only (the defaults below are what's used if unset) --- + +# Resolved relative to server/, where the backend runs from. +# DATABASE_URL="file:./data/app.db" + +# Root directory for all local project data (uploads, transcripts, renders). +# DATA_DIR="./data" + +# Port the Hono/Bun server listens on. The Vite client's dev server proxies +# /api/* requests here (see client/vite.config.mts). +# PORT=3001 + +# Origin allowed to make cross-origin requests in dev (the Vite dev server). +# CORS_ORIGIN="http://localhost:5173" + +# Overrides the `ffmpeg` binary used for rendering. Only needed if your +# default `ffmpeg` lacks libass (subtitle burn-in support) -- on Homebrew, +# `brew install ffmpeg-full` gets you one that has it, and this points at +# it instead of the default `ffmpeg` on PATH. Without this, "Burn in +# captions" will fail with "No such filter: 'subtitles'" if your default +# ffmpeg build doesn't include libass. FFPROBE_PATH does the same for +# ffprobe. +# FFMPEG_PATH="/opt/homebrew/opt/ffmpeg-full/bin/ffmpeg" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cefecda..4f742c2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -12,16 +12,14 @@ any of the tools below; see the README. For working on the code: ```bash -pnpm install # installs deps for client + server -cd server -cp .env.example .env # then fill in OPENAI_API_KEY -bunx prisma generate -cd .. -pnpm run dev # runs the Vite client and Bun backend together +pnpm install # installs deps for client + server +cp .env.example .env # then fill in OPENAI_API_KEY +pnpm --filter server db:generate # generates the Prisma client +pnpm run dev # runs the Vite client and Bun backend together ``` Open [http://localhost:5173](http://localhost:5173). See -[`server/.env.example`](./server/.env.example) for what each variable +[`.env.example`](./.env.example) for what each variable does. You'll also need [FFmpeg](https://ffmpeg.org/download.html) and diff --git a/README.md b/README.md index d318054..47b0a85 100644 --- a/README.md +++ b/README.md @@ -138,8 +138,9 @@ docker compose up --build ``` Open [http://localhost:3001](http://localhost:3001). On first launch the -app asks for your OpenAI API key. To skip that, create a `.env` file next to -`compose.yaml` containing `OPENAI_API_KEY=sk-...` before starting. +app asks for your OpenAI API key. To skip that, run `cp .env.example .env` +and set `OPENAI_API_KEY` before starting. It's the same root `.env` the +development setup uses. Projects, uploads and renders live in the `transcriptcut-data` Docker volume, so they survive restarts and rebuilds. To update, run @@ -159,16 +160,14 @@ For working on the code, with hot reload. Requires ```bash git clone https://github.com/amide-init/transcriptcut.git cd transcriptcut -pnpm install # installs deps for client + server -cd server -cp .env.example .env # then fill in OPENAI_API_KEY -bunx prisma generate -cd .. -pnpm run dev # runs the Vite client and Bun backend together +pnpm install # installs deps for client + server +cp .env.example .env # then fill in OPENAI_API_KEY +pnpm --filter server db:generate # generates the Prisma client +pnpm run dev # runs the Vite client and Bun backend together ``` Open [http://localhost:5173](http://localhost:5173). See -[`server/.env.example`](./server/.env.example) for what each variable +[`.env.example`](./.env.example) for what each variable does, and [`CONTRIBUTING.md`](./CONTRIBUTING.md) for more. One thing to know during development: diff --git a/claude.md b/claude.md index b931836..dae5e75 100644 --- a/claude.md +++ b/claude.md @@ -1491,7 +1491,7 @@ any deployment: * The OpenAI API key can be entered on first run through `client/src/routes/SetupRoute.tsx`, stored in `DATA_DIR/settings.json` (`server/src/lib/settings.ts`), which is checked before falling back to - `server/.env`'s `OPENAI_API_KEY`. + the root `.env`'s `OPENAI_API_KEY`. ## Docker @@ -1511,3 +1511,10 @@ schema. Choices worth knowing before changing them: burned-in text can differ slightly from a Mac. * compose publishes on `127.0.0.1` only, since there's no auth (section 18). +* There's one `.env`, at the repo root (`.env.example` documents it), + shared by both setups: the dev server loads it with + `bun --env-file=../.env` (Bun's own `.env` loading would look in + `server/`), the Prisma CLI through `prisma7.config.ts`, and compose + through `env_file`. compose pins the paths, port and FFmpeg binaries + over it, since dev values (relative paths, a Homebrew `FFMPEG_PATH`) + don't exist in the container. diff --git a/compose.yaml b/compose.yaml index 8c3ce8d..bf97f4e 100644 --- a/compose.yaml +++ b/compose.yaml @@ -10,11 +10,21 @@ services: - "127.0.0.1:3001:3001" volumes: - transcriptcut-data:/data - # Optional: OPENAI_API_KEY (and MAX_UPLOAD_MB) from a .env next to this - # file. Without one, the app asks for the key on first launch. + # Optional: OPENAI_API_KEY (and MAX_UPLOAD_MB) from the same root .env + # the dev server uses. Without one, the app asks for the key on first + # launch. env_file: - path: .env required: false + # Pinned over anything in .env, whose dev values (relative paths, a + # Homebrew FFMPEG_PATH) don't exist inside the container. + environment: + PORT: "3001" + DATA_DIR: /data + DATABASE_URL: file:/data/app.db + CLIENT_DIST_DIR: /app/client-dist + FFMPEG_PATH: /usr/local/bin/ffmpeg + FFPROBE_PATH: /usr/local/bin/ffprobe restart: unless-stopped volumes: diff --git a/docs/guide/getting-started.md b/docs/guide/getting-started.md index 0e6654c..fcfefe1 100644 --- a/docs/guide/getting-started.md +++ b/docs/guide/getting-started.md @@ -18,8 +18,9 @@ docker compose up --build ``` Open `http://localhost:3001`. On first launch the app asks -for your OpenAI API key. To skip that, create a `.env` file next to -`compose.yaml` containing `OPENAI_API_KEY=sk-...` before starting. +for your OpenAI API key. To skip that, run `cp .env.example .env` and set +`OPENAI_API_KEY` before starting. It's the same root `.env` the +development setup uses. Projects, uploads and renders live in the `transcriptcut-data` Docker volume, so they survive restarts and rebuilds. To update, run @@ -48,16 +49,14 @@ For working on the code, with hot reload. ```bash git clone https://github.com/amide-init/transcriptcut.git cd transcriptcut -pnpm install # installs deps for client + server -cd server -cp .env.example .env # then fill in OPENAI_API_KEY -bunx prisma generate -cd .. -pnpm run dev # runs the Vite client and Bun backend together +pnpm install # installs deps for client + server +cp .env.example .env # then fill in OPENAI_API_KEY +pnpm --filter server db:generate # generates the Prisma client +pnpm run dev # runs the Vite client and Bun backend together ``` Open `http://localhost:5173`. See -[`server/.env.example`](https://github.com/amide-init/transcriptcut/blob/main/server/.env.example) +[`.env.example`](https://github.com/amide-init/transcriptcut/blob/main/.env.example) for what each variable does. `MAX_UPLOAD_MB` raises or lowers the upload limit, which defaults to 10 GB. diff --git a/docs/guide/podcasting.md b/docs/guide/podcasting.md index 02035ea..b6eaec0 100644 --- a/docs/guide/podcasting.md +++ b/docs/guide/podcasting.md @@ -14,7 +14,7 @@ own, and AI output never edits your project without you reviewing it Upload the recording from **New project**. Long episodes are fine: - **Uploads up to 10 GB** by default (set `MAX_UPLOAD_MB` in - `server/.env` to change it). The file streams straight to disk, so size + `.env` to change it). The file streams straight to disk, so size is limited by free disk space, not memory. The upload button shows progress. - **Transcription runs in the background, in chunks.** The server diff --git a/server/.env.example b/server/.env.example deleted file mode 100644 index 5ae9797..0000000 --- a/server/.env.example +++ /dev/null @@ -1,34 +0,0 @@ -# Copy this file to .env and fill in the values below. -# Never commit your actual .env -- it's gitignored on purpose. -# Bun loads .env files natively -- no extra setup needed. - -# Required. Used server-side only (transcription via Whisper, and any -# future AI editing calls) -- never exposed to the browser. -OPENAI_API_KEY=sk-... - -# Resolved relative to the working directory the app runs from (server/). -DATABASE_URL="file:./data/app.db" - -# Root directory for all local project data (uploads, transcripts, renders). -DATA_DIR="./data" - -# Port the Hono/Bun server listens on. The Vite client's dev server proxies -# /api/* requests here (see client/vite.config.ts). -PORT=3001 - -# Origin allowed to make cross-origin requests in dev (the Vite dev -# server). Not needed once client and server are served from the same -# origin in production. -CORS_ORIGIN="http://localhost:5173" - -# Optional. Overrides the `ffmpeg` binary used for rendering. Only needed -# if your default `ffmpeg` lacks libass (subtitle burn-in support) -- on -# Homebrew, `brew install ffmpeg-full` gets you one that has it, and this -# points at it instead of the default `ffmpeg` on PATH. Without this, -# "Burn in captions" will fail with "No such filter: 'subtitles'" if your -# default ffmpeg build doesn't include libass. -# FFMPEG_PATH="/opt/homebrew/opt/ffmpeg-full/bin/ffmpeg" - -# Optional. Largest accepted video upload, in MB (default 10240 = 10GB). -# Uploads stream straight to disk, so this is about disk space, not memory. -# MAX_UPLOAD_MB=10240 diff --git a/server/package.json b/server/package.json index 2bde5a4..001192b 100644 --- a/server/package.json +++ b/server/package.json @@ -3,8 +3,8 @@ "private": true, "type": "module", "scripts": { - "dev": "bun --watch src/index.ts", - "start": "bun src/index.ts", + "dev": "bun --env-file=../.env --watch src/index.ts", + "start": "bun --env-file=../.env src/index.ts", "build": "bun build src/index.ts --outdir dist --target bun", "test": "vitest run", "test:watch": "vitest", diff --git a/server/prisma7.config.ts b/server/prisma7.config.ts index b62293a..8b0fcba 100644 --- a/server/prisma7.config.ts +++ b/server/prisma7.config.ts @@ -1,7 +1,13 @@ -// Bun loads .env files natively, so no dotenv/config import is needed here -// (this project's backend always runs via `bun`/`bunx`, never plain node). +import { existsSync } from "node:fs"; +import { fileURLToPath } from "node:url"; import { defineConfig } from "prisma/config"; +// The env file lives at the repo root (shared with docker compose), not +// next to this config, so the Prisma CLI has to load it itself. Values +// already in the environment win. +const rootEnv = fileURLToPath(new URL("../.env", import.meta.url)); +if (existsSync(rootEnv)) process.loadEnvFile(rootEnv); + export default defineConfig({ schema: "prisma/schema.prisma", migrations: { diff --git a/server/src/lib/settings.ts b/server/src/lib/settings.ts index 7ee275b..25e5b87 100644 --- a/server/src/lib/settings.ts +++ b/server/src/lib/settings.ts @@ -33,7 +33,7 @@ export function writeSettings(settings: Settings): void { /** * A key entered through the setup screen (stored in settings.json) takes - * priority over server/.env's OPENAI_API_KEY -- but local dev keeps + * priority over the root .env's OPENAI_API_KEY -- but local dev keeps * working unchanged via .env, since nothing writes settings.json unless * someone actually submits the setup form. */ diff --git a/server/src/routes/settings.ts b/server/src/routes/settings.ts index e00cca0..4601737 100644 --- a/server/src/routes/settings.ts +++ b/server/src/routes/settings.ts @@ -7,7 +7,7 @@ export const settingsRoute = new Hono(); // Never returns the key itself -- only whether one is configured. Checks // the same source getOpenAiApiKey() does (settings.json, falling back to -// server/.env's OPENAI_API_KEY) -- not just settings.json alone -- so +// the root .env's OPENAI_API_KEY) -- not just settings.json alone -- so // local dev (where the key lives in .env, and no one has ever hit the // setup screen) correctly reports "configured" instead of being wrongly // redirected to setup.