enimatek-nl/shuoshuo

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.

1

stars

12

commits

TypeScript

primary language

Aug 28, 2026

updated

README

Shuoshuo

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.

Screenshots

Video listVideo player with subtitles
Video list overviewVideo player with subtitles

Quick start (Docker)

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-turbo by default) into the volume, which can take a while.

How it works

  1. A video is uploaded through the web UI (POST /api/videos) and stored on disk with status queued.
  2. A background queue worker polls for queued videos, extracts audio with ffmpeg, and transcribes it with zhpipe-whisper (Whisper, large-v3-turbo by default).
  3. The transcription is saved as JSON next to the video file and the video status becomes ready.
  4. The web UI lists ready videos and plays them with the subtitles in sync.

Stack

LayerTechnology
BackendGo 1.25, net/http (std ServeMux), GORM + gormlite (pure-Go SQLite)
FrontendReact 18 + TypeScript, Vite, Mantine UI, @tanstack/react-query, react-router
Transcriptionffmpeg + zhpipe-whisper (external binaries, required at runtime)

Project layout

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

Prerequisites

  • Go (see backend/go.mod, currently 1.25)
  • Node.js 20+ (and bun or npm for the frontend)
  • ffmpeg on PATH
  • zhpipe-whisper on PATH

Development

Run the backend and frontend separately.

Backend

cd backend
go run ./cmd/server --port 8080 --data-dir ./data

Flags:

FlagDefaultDescription
--port8080HTTP listen port
--data-dir./dataDirectory 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.

Frontend

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.

Running everything together (single port)

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.

Configuration (transcription)

The transcription worker reads these environment variables:

VariableDefaultDescription
WHISPER_MODELbaseWhisper model name
WHISPER_MODEL_CACHE_PATH~/.cache/zhpipe/modelsWhere whisper models are cached

API

MethodPathDescription
POST/api/videosMultipart upload (field video); returns the created video record
GET/api/videos?status=readyList 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": "..."}.

Build the Docker image yourself

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

CI

  • .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.

Contributors

enimatek-nl

12 commits

enimatek-nl/shuoshuo

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.

1

stars

12

commits

TypeScript

primary language

Aug 28, 2026

updated

README

Shuoshuo

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.

Screenshots

Video listVideo player with subtitles
Video list overviewVideo player with subtitles

Quick start (Docker)

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-turbo by default) into the volume, which can take a while.

How it works

  1. A video is uploaded through the web UI (POST /api/videos) and stored on disk with status queued.
  2. A background queue worker polls for queued videos, extracts audio with ffmpeg, and transcribes it with zhpipe-whisper (Whisper, large-v3-turbo by default).
  3. The transcription is saved as JSON next to the video file and the video status becomes ready.
  4. The web UI lists ready videos and plays them with the subtitles in sync.

Stack

LayerTechnology
BackendGo 1.25, net/http (std ServeMux), GORM + gormlite (pure-Go SQLite)
FrontendReact 18 + TypeScript, Vite, Mantine UI, @tanstack/react-query, react-router
Transcriptionffmpeg + zhpipe-whisper (external binaries, required at runtime)

Project layout

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

Prerequisites

  • Go (see backend/go.mod, currently 1.25)
  • Node.js 20+ (and bun or npm for the frontend)
  • ffmpeg on PATH
  • zhpipe-whisper on PATH

Development

Run the backend and frontend separately.

Backend

cd backend
go run ./cmd/server --port 8080 --data-dir ./data

Flags:

FlagDefaultDescription
--port8080HTTP listen port
--data-dir./dataDirectory 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.

Frontend

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.

Running everything together (single port)

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.

Configuration (transcription)

The transcription worker reads these environment variables:

VariableDefaultDescription
WHISPER_MODELbaseWhisper model name
WHISPER_MODEL_CACHE_PATH~/.cache/zhpipe/modelsWhere whisper models are cached

API

MethodPathDescription
POST/api/videosMultipart upload (field video); returns the created video record
GET/api/videos?status=readyList 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": "..."}.

Build the Docker image yourself

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

CI

  • .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.

Contributors

enimatek-nl

12 commits

Languages

TypeScript

54.6%

Go

42.0%

Dockerfile

1.8%