A self-hosted video transcription web app. Upload a video, and it gets processed in the background into word-level subtitles (with pinyin and translation) that can be viewed alongside the video in the browser.
| Video list | Video player with subtitles |
|---|---|
![]() | ![]() |
The easiest way to run Shuoshuo is with the prebuilt Docker image, which
bundles the backend, the frontend, ffmpeg, and zhpipe-whisper:
docker run -d -p 3333:3333 -v shuoshuo-data:/data --name shuoshuo cybertim/shuoshuo
Then open http://localhost:3333 and upload a video.
The data directory (/data) holds the SQLite DB, uploads, and whisper models
(WHISPER_MODEL_CACHE_PATH), so the shuoshuo-data volume persists them
across container restarts.
The image is built and pushed to Docker Hub automatically when a release is published. The first transcription downloads the Whisper model (
large-v3-turboby default) into the volume, which can take a while.
POST /api/videos) and stored on disk
with status queued.ffmpeg, and transcribes it with zhpipe-whisper
(Whisper, large-v3-turbo by default).ready.| Layer | Technology |
|---|---|
| Backend | Go 1.25, net/http (std ServeMux), GORM + gormlite (pure-Go SQLite) |
| Frontend | React 18 + TypeScript, Vite, Mantine UI, @tanstack/react-query, react-router |
| Transcription | ffmpeg + zhpipe-whisper (external binaries, required at runtime) |
shuoshuo/
├── backend/
│ ├── cmd/server/
│ │ ├── main.go # entry point: flags, HTTP mux, SPA serving, graceful shutdown
│ │ └── route/ # HTTP handlers + request/response types
│ ├── internal/
│ │ ├── persist/ # GORM models + repository pattern (Video)
│ │ └── queue/ # background worker: ffmpeg -> zhpipe-whisper
│ ├── go.mod
│ └── go.sum
├── frontend/
│ ├── src/
│ │ ├── main.tsx # app bootstrap (Mantine, Router, React Query)
│ │ ├── main.css # global styles
│ │ ├── theme.ts # Mantine theme
│ │ ├── components/ # AppShell, VideoList, VideoPlayer
│ │ └── api/ # client.ts (fetch wrapper) + hooks.ts (React Query hooks)
│ ├── index.html
│ ├── package.json
│ └── vite.config.ts
├── .forgejo/workflows/flow.yml # Forgejo CI
├── .github/workflows/flow.yml # GitHub CI: build backend + frontend, push image to Docker Hub
└── Dockerfile
backend/go.mod, currently 1.25)bun or npm for the frontend)PATHPATHRun the backend and frontend separately.
cd backend
go run ./cmd/server --port 8080 --data-dir ./data
Flags:
| Flag | Default | Description |
|---|---|---|
--port | 8080 | HTTP listen port |
--data-dir | ./data | Directory for the SQLite DB and uploaded files |
--www-dir | (empty) | Frontend build directory; when set, the server serves the SPA alongside the API |
The SQLite DB and uploads/ directory are created automatically under --data-dir.
cd frontend
bun install # or: npm install
bun run dev # or: npm run dev
The Vite dev server runs on http://localhost:3000 and proxies /api/* to
http://localhost:8080.
Build the frontend, then point the backend at it:
cd frontend && bun run build
cd ../backend && go run ./cmd/server --port 8080 --data-dir ./data --www-dir ../frontend/dist
The server then serves the SPA (with client-side routing fallback) and the API on the same port.
The transcription worker reads these environment variables:
| Variable | Default | Description |
|---|---|---|
WHISPER_MODEL | base | Whisper model name |
WHISPER_MODEL_CACHE_PATH | ~/.cache/zhpipe/models | Where whisper models are cached |
| Method | Path | Description |
|---|---|---|
POST | /api/videos | Multipart upload (field video); returns the created video record |
GET | /api/videos?status=ready | List videos (status filter, defaults to ready) |
GET | /api/videos/{id} | Get a single video by ID |
GET | /api/videos/serve/{filename} | Stream the video file |
GET | /api/subtitles/{filename} | Get the subtitle JSON for a video |
Errors are returned as {"error": "...", "details": "..."}.
The Dockerfile bundles the backend binary, the frontend build,
and zhpipe-whisper, and runs everything on port 3333. Build the backend and
frontend artifacts first (as the CI workflow does), then:
docker build -t shuoshuo .
docker run -p 3333:3333 -v shuoshuo-data:/data shuoshuo
.github/workflows/flow.yml builds the backend (Go) and frontend (Node),
then builds and pushes a Docker image to
Docker Hub.
It runs when a release is published..forgejo/workflows/flow.yml builds the backend and frontend, then builds
a local image with podman. It runs on workflow_dispatch.12 commits
TypeScript
54.6%
Go
42.0%
Dockerfile
1.8%
A self-hosted video transcription web app. Upload a video, and it gets processed in the background into word-level subtitles (with pinyin and translation) that can be viewed alongside the video in the browser.
| Video list | Video player with subtitles |
|---|---|
![]() | ![]() |
The easiest way to run Shuoshuo is with the prebuilt Docker image, which
bundles the backend, the frontend, ffmpeg, and zhpipe-whisper:
docker run -d -p 3333:3333 -v shuoshuo-data:/data --name shuoshuo cybertim/shuoshuo
Then open http://localhost:3333 and upload a video.
The data directory (/data) holds the SQLite DB, uploads, and whisper models
(WHISPER_MODEL_CACHE_PATH), so the shuoshuo-data volume persists them
across container restarts.
The image is built and pushed to Docker Hub automatically when a release is published. The first transcription downloads the Whisper model (
large-v3-turboby default) into the volume, which can take a while.
POST /api/videos) and stored on disk
with status queued.ffmpeg, and transcribes it with zhpipe-whisper
(Whisper, large-v3-turbo by default).ready.| Layer | Technology |
|---|---|
| Backend | Go 1.25, net/http (std ServeMux), GORM + gormlite (pure-Go SQLite) |
| Frontend | React 18 + TypeScript, Vite, Mantine UI, @tanstack/react-query, react-router |
| Transcription | ffmpeg + zhpipe-whisper (external binaries, required at runtime) |
shuoshuo/
├── backend/
│ ├── cmd/server/
│ │ ├── main.go # entry point: flags, HTTP mux, SPA serving, graceful shutdown
│ │ └── route/ # HTTP handlers + request/response types
│ ├── internal/
│ │ ├── persist/ # GORM models + repository pattern (Video)
│ │ └── queue/ # background worker: ffmpeg -> zhpipe-whisper
│ ├── go.mod
│ └── go.sum
├── frontend/
│ ├── src/
│ │ ├── main.tsx # app bootstrap (Mantine, Router, React Query)
│ │ ├── main.css # global styles
│ │ ├── theme.ts # Mantine theme
│ │ ├── components/ # AppShell, VideoList, VideoPlayer
│ │ └── api/ # client.ts (fetch wrapper) + hooks.ts (React Query hooks)
│ ├── index.html
│ ├── package.json
│ └── vite.config.ts
├── .forgejo/workflows/flow.yml # Forgejo CI
├── .github/workflows/flow.yml # GitHub CI: build backend + frontend, push image to Docker Hub
└── Dockerfile
backend/go.mod, currently 1.25)bun or npm for the frontend)PATHPATHRun the backend and frontend separately.
cd backend
go run ./cmd/server --port 8080 --data-dir ./data
Flags:
| Flag | Default | Description |
|---|---|---|
--port | 8080 | HTTP listen port |
--data-dir | ./data | Directory for the SQLite DB and uploaded files |
--www-dir | (empty) | Frontend build directory; when set, the server serves the SPA alongside the API |
The SQLite DB and uploads/ directory are created automatically under --data-dir.
cd frontend
bun install # or: npm install
bun run dev # or: npm run dev
The Vite dev server runs on http://localhost:3000 and proxies /api/* to
http://localhost:8080.
Build the frontend, then point the backend at it:
cd frontend && bun run build
cd ../backend && go run ./cmd/server --port 8080 --data-dir ./data --www-dir ../frontend/dist
The server then serves the SPA (with client-side routing fallback) and the API on the same port.
The transcription worker reads these environment variables:
| Variable | Default | Description |
|---|---|---|
WHISPER_MODEL | base | Whisper model name |
WHISPER_MODEL_CACHE_PATH | ~/.cache/zhpipe/models | Where whisper models are cached |
| Method | Path | Description |
|---|---|---|
POST | /api/videos | Multipart upload (field video); returns the created video record |
GET | /api/videos?status=ready | List videos (status filter, defaults to ready) |
GET | /api/videos/{id} | Get a single video by ID |
GET | /api/videos/serve/{filename} | Stream the video file |
GET | /api/subtitles/{filename} | Get the subtitle JSON for a video |
Errors are returned as {"error": "...", "details": "..."}.
The Dockerfile bundles the backend binary, the frontend build,
and zhpipe-whisper, and runs everything on port 3333. Build the backend and
frontend artifacts first (as the CI workflow does), then:
docker build -t shuoshuo .
docker run -p 3333:3333 -v shuoshuo-data:/data shuoshuo
.github/workflows/flow.yml builds the backend (Go) and frontend (Node),
then builds and pushes a Docker image to
Docker Hub.
It runs when a release is published..forgejo/workflows/flow.yml builds the backend and frontend, then builds
a local image with podman. It runs on workflow_dispatch.12 commits
TypeScript
54.6%
Go
42.0%
Dockerfile
1.8%