No description
  • TypeScript 54.6%
  • Go 41.9%
  • Dockerfile 1.8%
  • HTML 0.8%
  • JavaScript 0.7%
  • Other 0.2%
Find a file
2026-08-28 22:00:51 +02:00
.forgejo/workflows now with host added 2026-07-19 09:35:58 +02:00
.github/workflows added screenshots and github flow 2026-08-28 21:35:39 +02:00
backend small improvements 2026-07-19 16:10:16 +02:00
frontend first commit 2026-07-19 09:01:34 +02:00
.gitignore first commit 2026-07-19 09:01:34 +02:00
Dockerfile zhpipe artifact update 2026-08-28 21:53:41 +02:00
overview.png added screenshots and github flow 2026-08-28 21:35:39 +02:00
README.md readme update 2026-08-28 22:00:51 +02:00
video.png added screenshots and github flow 2026-08-28 21:35:39 +02:00

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 list Video player with subtitles
Video list overview Video 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

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)

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:

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.

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:

Variable Default Description
WHISPER_MODEL base Whisper model name
WHISPER_MODEL_CACHE_PATH ~/.cache/zhpipe/models Where whisper models are cached

API

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": "..."}.

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.