# Subvert: self-hosted Whisper subtitles, chapters and summaries in one Docker image

> Subvert wraps FFmpeg, OpenAI's Whisper transcription and a ChatGPT pass for chapters and summaries into a single self-hosted Docker container. It is convenient for solo video work, but the README is thin on production concerns.

**aschmelyun/subvert** — Generate subtitles, summaries, and chapters from videos in seconds

- Repository: https://github.com/aschmelyun/subvert
- Website: https://subvert.dev
- Stars: 869 · Forks: 60
- Language: PHP
- License: MIT
- Published: 2026-09-14 · Updated: 2026-09-14 · Language: en
- Canonical page: https://hysenlabs.com/projects/aschmelyun-subvert

## What Subvert actually does with a video file

Subvert targets one narrow job: taking a video file and returning a subtitle track, plus optionally a chapter list and a short summary. The README describes it as generating subtitles, chapters and summaries of videos "in seconds with the help of OpenAI". The intended user is someone who edits or publishes video and wants a transcript without paying for a hosted transcription service or wiring up FFmpeg and an ASR model by hand. The repository topics list confirms the scope: chatgpt, openai, transcription, translation, video-editing, whisper. It is not a video editor, not a captioning style tool, and not a media asset manager. It is a single-purpose conversion step that you run yourself. The README also carries a warning that the project is "very much a work-in-progress" and asks users to file issues for bugs, which sets the expectation for stability.

## The pipeline: FFmpeg, Whisper, then ChatGPT

The mechanism is documented in the README's "How it works" section and is visible in the Dockerfile. You select a video file in the web interface and choose whether to also generate chapters and a summary. The video goes to an API, the audio is extracted with FFMpeg, and that audio is sent to OpenAI's Whisper model for transcription into the common VTT format. If you asked for chapters or a summary, the transcript is then sent to a ChatGPT model, which produces chapters of the length you requested and a brief summary sized to fit something like a YouTube description. The Dockerfile confirms the local half of that pipeline: the image is built from php:8.2-alpine, installs ffmpeg with apk, and declares two volumes at /var/www/storage/app/audio and /var/www/storage/app/video. Those directories are where extracted audio and uploaded video land, so they are the two paths worth backing up or mounting to persistent storage. The OpenAI half is opaque from the outside: the README does not name which Whisper or ChatGPT model versions are called, nor whether the transcription endpoint is the file-based or streaming one.

## Running Subvert from the published Docker image

The README gives a one-line command to start the container. It maps port 80 on your machine to port 8080 inside the container and passes the OpenAI key as an environment variable. The key shown is a placeholder, not a real credential.

```bash
docker run -it -p 80:8080 -e OPENAI_API_KEY=sk-123abc aschmelyun/subvert
```

After that, the README states the server boots and the application is available at http://localhost. Two more environment variables are documented for tuning the container: UPLOAD_MAX_FILESIZE changes PHP's upload limit and defaults to 256M, and MEMORY_LIMIT changes PHP's memory limit and defaults to 512M. Passing them looks like this:

```bash
docker run -it -p 80:8080 \
  -e OPENAI_API_KEY=sk-123abc \
  -e UPLOAD_MAX_FILESIZE=1G \
  -e MEMORY_LIMIT=1G \
  aschmelyun/subvert
```

The repository also ships a docker-compose.yml that runs the image alongside an nginx TLS proxy. In that file the subvert service sets OPENAI_API_KEY, UPLOAD_MAX_FILESIZE=256M and MEMORY_LIMIT=512M, and the proxy service sets UPSTREAM to subvert:8080 and NGINX_CLIENT_MAX_BODY_SIZE to 256M, with a comment telling you to change it to match Subvert's UPLOAD_MAX_FILESIZE. If you raise one limit and not the other, large uploads will fail before they reach the application.

## Building from source needs PHP 8.1, npm and a startup script

The README offers a second route for people who would rather not use the published image. With PHP 8.1+ and npm installed locally, you check out the repository, navigate into the src directory and run ./startup.sh. The README says you can instead run the commands inside that script individually for the same result, which is useful when the script assumes something your machine does not have. This is the path to take if you intend to modify the application rather than just run it, because the Dockerfile copies ./src into /var/www and runs composer install, npm install and npm run build at image build time. Note the version split: the README asks for PHP 8.1 or newer, while the Dockerfile builds on php:8.2-alpine. Both are consistent with the README, but they are not the same target.

## The HTTP-only default and other limits you should plan around

The README's deployment note is blunt: the image currently only exposes the insecure :80 http port. That means the plain docker run command serves the interface over unencrypted HTTP. If you put it on a network you do not control, you are uploading video and an API key over a connection with no transport security. The provided docker-compose.yml addresses this by pairing the app with danieldent/nginx-ssl-proxy and setting SERVERNAME to a domain you own, but that file is a starting point, not a finished deployment: it contains a placeholder domain and a placeholder API key. The second limit is the third-party dependency. Transcription and chapter generation both go to OpenAI, so the tool is unusable without an API key and your audio leaves your machine. The third is that uploads and memory are capped by two separate PHP settings that must be raised together, and the compose file's NGINX_CLIENT_MAX_BODY_SIZE comment exists precisely because mismatched limits are an easy mistake. Finally, the README documents no rollback procedure, no database migration story and no upgrade steps between versions.

## How Subvert compares with running Whisper yourself

The obvious alternative is running an OpenAI Whisper model locally, either through the reference implementation or through a wrapper such as whisper.cpp, and doing the chapter and summary step separately. The difference in approach is where the compute and the data live. Subvert sends audio to OpenAI's API and uses a ChatGPT model to derive chapters and summary from the transcript; a local Whisper setup keeps the audio on your machine but gives you only the transcript, leaving chaptering and summarisation to you or to another tool. Subvert's trade-off is convenience and cost model: one container, one key, and you pay per API call rather than per GPU hour. The local route's trade-off is setup effort and hardware, with no per-minute billing and no third party seeing the audio. For a handful of videos a month, Subvert's model is simpler. For a large archive, or for material you cannot send to a third party, the local route is the one that fits.

## Maintenance, releases and the MIT licence

The repository is not archived, and the last push was on 2026-05-15. That is recent activity on the default branch, but it is worth separating from the release history: the most recent tagged release listed is v1.0.9 from 2023-04-16, preceded by v1.0.8 and v1.0.7 the same day. So the version you pull from Docker Hub reflects a 2023 tag while the repository has seen commits since. The README's own "work-in-progress" warning is consistent with that gap. Upgrading means pulling a newer image and restarting the container; the README does not describe any state migration, and the SQLite file at database/database.sqlite is created inside the image at build time, so anything stored there lives with the container unless you mount it. The licence is MIT, stated in the README and shipped as LICENSE.md. MIT is permissive and imposes no copyleft obligation on your own code, but it also means the project offers no warranty, and the OpenAI API terms governing the audio you send are a separate matter from the project licence. This is not legal advice.

## Conclusion

Adopt Subvert if you want a local, self-hosted pipeline that turns a video into a VTT transcript plus optional chapters and summary, and you already hold an OpenAI API key and have Docker installed. Skip it if you need HTTPS out of the box, a documented upgrade path between releases, or a tool that does not send your audio to a third-party API. Before relying on it, verify the OpenAI key is accepted, confirm the UPLOAD_MAX_FILESIZE and MEMORY_LIMIT values match the size of your videos, and check the repository's latest commits against the v1.0.9 release from April 2023.

## FAQ

### How does Subvert work?

The README states that your video is sent to an API where FFMpeg extracts the audio, which is then transcribed by OpenAI's Whisper model into VTT format. If you select chapters or a summary, that transcript is passed to a ChatGPT model to produce them.

### How do I install and start Subvert?

You need Docker installed and an OpenAI API key. The README gives a single command, docker run -it -p 80:8080 -e OPENAI_API_KEY=sk-123abc aschmelyun/subvert, after which the app is available at http://localhost.

### Can I run Subvert from source instead of Docker?

Yes, if you have PHP 8.1 or newer and npm installed. The README says to check out the repository, navigate to the src directory and run ./startup.sh, or to run the commands inside that script individually.

### Does Subvert support HTTPS?

Not from the image alone. The README notes that the image currently only exposes the insecure :80 http port. The repository's docker-compose.yml pairs it with danieldent/nginx-ssl-proxy and sets SERVERNAME to your domain, but you must replace the placeholder values.

### What are the upload size and memory limits in Subvert?

Two environment variables control them. UPLOAD_MAX_FILESIZE changes PHP's upload limit and defaults to 256M, and MEMORY_LIMIT changes PHP's memory limit and defaults to 512M. In the compose file, NGINX_CLIENT_MAX_BODY_SIZE must be changed to match UPLOAD_MAX_FILESIZE.

## Sources

- [aschmelyun/subvert on GitHub](https://github.com/aschmelyun/subvert)
- [License: MIT](https://github.com/aschmelyun/subvert/blob/main/LICENSE)
- [Project website](https://subvert.dev)
- [README](https://github.com/aschmelyun/subvert/blob/main/README.md)
- [Releases](https://github.com/aschmelyun/subvert/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/aschmelyun-subvert
