Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -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
43 changes: 43 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -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"
23 changes: 10 additions & 13 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,29 +7,26 @@ 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:
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
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
[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

Expand Down
53 changes: 53 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -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"]
52 changes: 31 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,40 +124,50 @@ 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.

### 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`:
### With Docker

```bash
git clone https://github.com/amide-init/transcriptcut.git
cd transcriptcut
./scripts/setup-mac.sh
docker compose up --build
```

Then `pnpm run dev` from the repo root. Safe to re-run -- it only
installs what's missing and never overwrites an existing `.env`.
Open [http://localhost:3001](http://localhost:3001). On first launch the
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
`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.

### Manual setup (any platform)
### 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
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
git clone https://github.com/amide-init/transcriptcut.git
cd transcriptcut
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:
Expand Down
32 changes: 29 additions & 3 deletions claude.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -1491,4 +1491,30 @@ 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

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).
* 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.
33 changes: 33 additions & 0 deletions compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# 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 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:
transcriptcut-data:
# Fixed name, so it doesn't depend on the folder the repo is cloned into.
name: transcriptcut-data
4 changes: 2 additions & 2 deletions docs/community.md
Original file line number Diff line number Diff line change
Expand Up @@ -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):

Expand Down
Loading
Loading