CutScene lets you create short clips from media that is currently playing in Plex. It includes a browser UI and an HTTP API.
Copy config.example.yaml to config.yaml in the same directory and set the values for your Plex server. The Plex token is used by CutScene to read server metadata; see Plex's instructions for finding an authentication token.
Set api.domain to the browser-reachable base URL for CutScene. It is passed to Plex as the OAuth/PIN authentication forward URL, so it must be the URL users can return to after signing in (not a private container hostname).
The default docker-compose.yaml uses the GitHub Container Registry image. Start it with:
docker compose upIf you changed the listen address or port, update the port mapping in docker-compose.yaml.
Completed renders are promoted to durable clips. Configure storage.root (the
sample uses /data) as the local application-data directory. CutScene stores
the SQLite clip metadata database there, MP4s below clips/, and the
clip-token.key encryption key. The default Compose file mounts the named
cutscene-data volume at /data; it deliberately does not mount /tmp, which
contains transient render jobs and may be cleared on restart.
If storage.root is omitted it defaults to ./data relative to the CutScene
process working directory. If storage.database is omitted it defaults to
clips.sqlite3 directly below that root; configured database paths must remain
relative to the root.
Do not use docker compose down -v unless you intend to delete the durable
volume and all saved clips.
The storage root and clip directory are created with owner-only permissions;
the root and clip directory use 0700, and clip-token.key must be exactly
0600. Restrict access to that key as you would any capability secret. Keep
the database, MP4s, and key in the same encrypted backup set. Before upgrading
from an older release, take a complete encrypted backup of the storage root;
the key is required to preserve encrypted share links. Backups should be taken
with CutScene stopped (or with a filesystem/database snapshot) so metadata and
files represent one point in time. If the key is missing or wrong, startup
refuses to run until the matching key is restored; it does not generate a
replacement key. The MP4s may remain, but the service will not expose or serve
encrypted share links without that matching key.
To use a host path or a custom volume, set storage.root and mount that exact
path into the container. For example, with storage.root: /srv/cutscene, use
/host/cutscene:/srv/cutscene rather than mounting /data; the database path
is relative to the root by default and absolute or escaping paths are rejected.
Startup validates the restored schema, key, encrypted tokens, and clip files.
A missing database, missing/wrong key, or ambiguous restore fails closed and
leaves existing MP4s untouched; restore the matching backup and retry.
Durable clip capacity is intentionally unbounded by application policy. Monitor the mounted filesystem and plan disk alerts/backups; the existing queue, per-owner, FFmpeg, and transient render limits still apply.
On startup, CutScene preflights clip metadata and files before any migration or reconciliation. Mismatched, stale, partial, or unreferenced durable state fails closed without deleting either side. A legacy Phase 1 hash-only database is migrated to encrypted token storage; because the old raw token was not retained, those legacy clips receive newly generated share tokens while their metadata and MP4s are preserved. Deleting a clip is a hard delete of both its SQLite metadata and MP4 bytes and is permanent.
Open the CutScene URL in a browser and choose Log in with Plex. CutScene uses Plex's browser authentication flow and keeps the authenticated session in the browser. The session, preview, and render endpoints require that authenticated session; an HTTP client must preserve the session cookie established by its Plex login.
The default libx264 codec uses software encoding. On Linux, set ffmpeg.codec to h264_vaapi for VAAPI hardware encoding. The host and container need access to a usable render device under /dev/dri; the supplied docker-compose.gpu.yaml mounts /dev/dri/renderD128:
docker compose -f docker-compose.yaml -f docker-compose.gpu.yaml upThe VAAPI setup is tested with AMD GPUs on Linux. Intel QuickSync through VAAPI may work but is untested. h264_nvenc is also available for Nvidia, but requires a working Nvidia driver, the Nvidia Container Toolkit, and Docker configured to expose the GPU; the supplied GPU override only provides the VAAPI/DRI device.
After authentication, choose an active Plex session in the UI, preview and trim it, then submit the render. Completed clips are downloadable from the render-job status panel.
The examples below assume an authenticated session cookie in $COOKIE:
BASE=http://127.0.0.1:8080
curl -sS -b "$COOKIE" "$BASE/sessions"Use an active session's ratingKey and media/part ID as mediaId to create a job. The request must be JSON:
curl -i -b "$COOKIE" \
-H 'Content-Type: application/json' \
-d '{
"ratingKey": "100151",
"mediaId": 123456,
"fromMs": 300000,
"toMs": 305000,
"subtitleIndex": -1,
"height": 720,
"qp": 24,
"audioMode": "standard"
}' \
"$BASE/render-jobs"Creation returns 202 Accepted, a JSON job object, and a Location header such as /render-jobs/<id>. Poll that URL until status is succeeded or failed (or the job expires):
curl -sS -b "$COOKIE" "$BASE/render-jobs/<id>"
curl -fL -b "$COOKIE" "$BASE/render-jobs/<id>/download" -o clip.mp4Jobs are private to the authenticated user. A successful output is temporary and normally expires within one hour; use expiresAt and download it before expiry. An expired job or download returns 410 Gone.
If render capacity or temporary service/storage capacity is unavailable, creation can return 429 or 503 with a Retry-After header. Wait for that interval before retrying rather than submitting a tight loop.
subtitleIndex:-1for no subtitles, or the selected subtitle index.height: output height in pixels;0keeps the source height. Valid explicit heights are 144–2160 and even.qp: encoder quantization parameter,0for the encoder default, or a value from 0–51.audioMode:standard(original audio),dialogue(centred-dialogue boost), ordialogue_normalized(dialogue boost plus loudness normalization). If omitted, it defaults tostandard.
The selected range must be ordered, non-negative, and no longer than 15 minutes.
ffmpeg.concurrency controls the maximum number of concurrent FFmpeg processes. It defaults to 2 when omitted or non-positive, and the limit is shared by previews, subtitle preparation, and render jobs.
CutScene includes an optional semantic-search proof of concept with shared pgvector PostgreSQL infrastructure and one GPU embedding backend. Choose exactly one mutually exclusive overlay:
- NVIDIA: Hugging Face Text Embeddings Inference (TEI),
BAAI/bge-base-en-v1.5, servicetei. - AMD: vLLM ROCm,
BAAI/bge-base-en-v1.5, serviceembeddings.
Neither PostgreSQL nor the embedding service publishes a host port; CutScene reaches both over Compose's private network. The shared docker-compose.semantic-search.yaml contains PostgreSQL only. Do not combine the NVIDIA and AMD overlays.
Install the NVIDIA Container Toolkit, configure Docker with it, and verify both the host and Docker GPU paths:
nvidia-smi
docker run --rm --gpus all nvidia/cuda:12.4.1-base-ubuntu22.04 nvidia-smiIf the Docker check cannot see the GPU, configure the runtime with sudo nvidia-ctk runtime configure --runtime=docker and restart Docker, then repeat the check. The default TEI tag, 86-1.9, targets NVIDIA Ampere hardware such as RTX 30-series cards and A10s. TEI CUDA tags and supported GPU architectures change over time; select a compatible tag in docker-compose.semantic-search.nvidia.yaml for another architecture.
Use the NVIDIA configuration in config.example.yaml: embeddings_provider: tei and embeddings_url: http://tei:80. Start exactly this combination:
cp config.example.yaml config.yaml
# edit config.yaml: set plex.host, plex.token, and api.domain
docker compose -f docker-compose.yaml -f docker-compose.semantic-search.yaml -f docker-compose.semantic-search.nvidia.yaml up -d
docker compose -f docker-compose.yaml -f docker-compose.semantic-search.yaml -f docker-compose.semantic-search.nvidia.yaml psInstall a ROCm release compatible with the host GPU and verify the host and container device paths:
rocminfo
rocm-smi
docker run --rm --device=/dev/kfd --device=/dev/dri rocm/pytorch:latest rocminfoIf the container check cannot see the GPU, ensure /dev/kfd and /dev/dri exist, the invoking user can access their render/video groups, and Docker is allowed to pass those devices. Some AMD architectures require an HSA_OVERRIDE_GFX_VERSION; set that architecture-specific value in docker-compose.semantic-search.amd.yaml only when required by the installed ROCm release.
For AMD, change the sample configuration to embeddings_provider: openai and embeddings_url: http://embeddings:8000. The AMD overlay passes the model as vLLM's positional argument and explicitly selects its pooling/embed runner; this is required by current vLLM images. Start exactly this mutually exclusive combination:
cp config.example.yaml config.yaml
# edit config.yaml: set Plex values and change semantic_search to the AMD values
docker compose -f docker-compose.yaml -f docker-compose.semantic-search.yaml -f docker-compose.semantic-search.amd.yaml up -d
docker compose -f docker-compose.yaml -f docker-compose.semantic-search.yaml -f docker-compose.semantic-search.amd.yaml psWait for postgres and the selected embedding service (tei for NVIDIA or embeddings for AMD) to report healthy before indexing. The model loads during startup, so the embedding healthcheck can take a while. For example, inspect the shared database health with pg_isready -U cutscene -d cutscene in the postgres container and inspect the selected service with its /health endpoint. The sample semantic_search.postgres_dsn uses the internal POC service name and a non-production password; keep it synchronized with POSTGRES_PASSWORD and do not reuse it outside this local setup.
Use a PostgreSQL logical dump rather than copying the Docker volume. The procedure below preserves the pgvector tables and embeddings while allowing the NAS to use its own Docker storage. Run the commands in Bash, and replace the angle-bracket placeholders. Do not put passwords, Plex tokens, or session cookies in commands.
-
Stop writes before exporting. Let any in-flight indexing request finish, do not start another one, and stop CutScene so no new index writes can begin. Keep using the same mutually exclusive overlay that is currently selected:
# Choose exactly one: COMPOSE=(docker compose -f docker-compose.yaml -f docker-compose.semantic-search.yaml -f docker-compose.semantic-search.nvidia.yaml) # AMD alternative: # COMPOSE=(docker compose -f docker-compose.yaml -f docker-compose.semantic-search.yaml -f docker-compose.semantic-search.amd.yaml) "${COMPOSE[@]}" stop cutscene
If another process can write to the
postgresservice, stop or pause it too. The stopped application is the POC's indexing pause; do not export while indexing is active. -
Create a custom-format dump from the source
postgrescontainer. The shared Compose service is namedpostgres;pg_dump -Fcwrites a compressed, restorable custom-format archive. The database and user are read from the container's Compose environment, so no credential is included in the command:"${COMPOSE[@]}" exec -T postgres sh -lc 'pg_dump -Fc -U "$POSTGRES_USER" -d "$POSTGRES_DB"' > semantic-search.dump chmod 600 semantic-search.dump
Check that the dump file is non-empty and keep it on encrypted, access-controlled storage during the transfer. Do not delete the source database until the NAS restore and verification succeed.
-
Securely copy the dump to the NAS. Use an already configured SSH key or another approved secure SSH mechanism; the placeholders below are not credentials:
scp -p semantic-search.dump <nas-user>@<nas-host>:<nas-secure-directory>/semantic-search.dump # Or, for a resumable transfer: rsync -a --chmod=F600 semantic-search.dump <nas-user>@<nas-host>:<nas-secure-directory>/semantic-search.dump
-
Prepare PostgreSQL on the NAS. Place the repository Compose files on the NAS and use the same
docker-compose.semantic-search.yaml. It currently pinspgvector/pgvector:pg16; start the same pgvector/PostgreSQL major version on the NAS before restoring. Use the same selected overlay (NVIDIA or AMD) if the NAS will run CutScene and embeddings, and never combine both overlays. Start only the database first:# Use the matching NVIDIA or AMD COMPOSE assignment from step 1. "${COMPOSE[@]}" up -d postgres "${COMPOSE[@]}" ps postgres
Confirm that the
postgrescontainer is healthy. Do not start CutScene until the restore and configuration checks below are complete. If the NAS Compose files use a different PostgreSQL/pgvector major version, stop and use the source major version first; perform a separate PostgreSQL upgrade after migration. -
Restore the archive on the NAS. Copy the archive into the NAS
postgrescontainer, then restore into the existing Compose database.--clean --if-existsremoves objects present in the archive before recreating them;--no-owneravoids requiring source host role IDs."${COMPOSE[@]}" cp <nas-secure-directory>/semantic-search.dump postgres:/tmp/semantic-search.dump "${COMPOSE[@]}" exec -T postgres sh -lc 'pg_restore --clean --if-exists --no-owner --exit-on-error -U "$POSTGRES_USER" -d "$POSTGRES_DB" /tmp/semantic-search.dump' "${COMPOSE[@]}" exec -T postgres rm -f /tmp/semantic-search.dump
-
Verify the extension, table, and row count. These checks use the current schema's
vectorextension andsubtitle_chunkstable. The count should match the source count (record the source count before migration if an exact comparison is needed):"${COMPOSE[@]}" exec -T postgres sh -lc 'psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -c "SELECT extname, extversion FROM pg_extension WHERE extname = '\''vector'\'';"' "${COMPOSE[@]}" exec -T postgres sh -lc 'psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -c "SELECT to_regclass('\''public.subtitle_chunks'\'') AS subtitle_chunks;"' "${COMPOSE[@]}" exec -T postgres sh -lc 'psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -c "SELECT count(*) AS subtitle_chunk_count FROM public.subtitle_chunks;"'
A missing
vectorextension, a nullsubtitle_chunksrelation, or an unexpected count means the migration is not verified. Keep the source dump and investigate before starting indexing on the NAS. -
Update and verify the NAS CutScene configuration. Ensure the NAS
config.yamlpoints to the internal NAS service names, notlocalhost: usepostgresinsemantic_search.postgres_dsn,teiwithembeddings_provider: teifor the NVIDIA overlay, orembeddingswithembeddings_provider: openaifor the AMD overlay. Also ensure the NAS uses the same Plex server and owner identity/Plex account semantics as the desktop: preserve the corresponding owner account configuration and Plex access, rather than initializing the NAS against a different Plex owner or account. Keep Plex tokens in the protected NAS config file, never in shell history or migration commands. Start the full, matching Compose combination only after these checks pass, then leave indexing paused until a small search/index smoke test succeeds.
Moving the named semantic-search-postgres-data Docker volume directly is possible only with careful full-volume shutdown and compatible Docker storage paths, ownership, PostgreSQL major version, and host architecture. It is less portable, easier to make inconsistent, and harder to validate than pg_dump -Fc/pg_restore; it is not recommended for this migration.
Sign in to CutScene, select one Plex library source, and use that source's ratingKey, mediaId, and partId for the synchronous index request. Preserve the authenticated session cookie in $COOKIE:
BASE=http://127.0.0.1:8080
# Select a source in the CutScene library UI and set these three source values.
RATING_KEY='<selected-source-rating-key>'
MEDIA_ID='<selected-source-media-id>'
PART_ID='<selected-source-part-id>'
curl -i -b "$COOKIE" -H 'Content-Type: application/json' \
-d "{\"ratingKey\":\"$RATING_KEY\",\"mediaId\":$MEDIA_ID,\"partId\":$PART_ID}" \
-X POST "$BASE/subtitle-search/index"Indexing is synchronous in this POC; keep the request open until it completes. Search the indexed source with:
curl -sS -b "$COOKIE" \
"$BASE/subtitle-search?q=$(python3 -c 'import urllib.parse,sys; print(urllib.parse.quote(sys.argv[1]))' 'a line to find')"The POC embeds English text only. Plex must be reachable from the CutScene container over HTTP/HTTPS so subtitle media can be read. PGS/image subtitles and external subtitle tracks are excluded. Only one source is indexed per synchronous request; this is not yet a background or multi-source indexing system.
The docker-compose.build.yaml file builds the Docker image from source:
docker compose -f docker-compose.yaml -f docker-compose.build.yaml up --buildAlternatively, compile or run directly with go build ./... or go run ./... (Go >= 1.22 recommended).
