Files
8-cut/docs/plans/2026-04-16-server-api-design.md
T
2026-04-16 13:20:13 +02:00

6.5 KiB

8-cut Server API Design

Goal

Run 8-cut as a FastAPI server on Unraid (Docker) so a Tauri desktop client on Mac can edit remotely over WireGuard — no file transfers, no auth.

Architecture

Unraid (Docker container):
  FastAPI + ffmpeg + SQLite
  ├── /api/files         list videos from mounted volumes
  ├── /api/stream/{path} transcoded video (cached, no audio)
  ├── /api/audio/{path}  full-quality audio (cached, passthrough)
  ├── /api/video/{path}  raw file (for reference/download)
  ├── /api/markers       CRUD markers per profile
  ├── /api/profiles      list/create profiles
  ├── /api/export        trigger + manage exports
  ├── /api/labels        label history
  ├── /api/hidden        hidden file management
  └── ws://…/ws/export   real-time export progress

Mac (Tauri + Svelte + libmpv):
  ├── mpv plays stream URL (video) + audio URL separately
  ├── Canvas timeline + crop overlay + keyframes
  ├── Full UI: profiles, subprofiles, settings
  └── Stateless — all state lives on server

Docker mounts

Mount Purpose Env var
/videos Source video files (read-only) MEDIA_DIRS
/exports Export output EXPORT_DIR
/data SQLite DB + transcode cache DB_PATH, CACHE_DIR

MEDIA_DIRS supports multiple paths: /videos1,/videos2.

Video streaming with transcode cache

The client needs low-bitrate video for scrubbing over the network but full-quality audio for accurate editing.

Flow:

  1. Client requests /api/stream/{path}?quality=low
  2. Server checks cache: {CACHE_DIR}/{quality}/{hash}.mp4
  3. If cached → serve with range requests (instant seeking)
  4. If not → start background ffmpeg transcode, return 202 Accepted with job ID
  5. Client polls or gets WebSocket notification when ready
  6. Audio: /api/audio/{path} extracts audio (passthrough, fast) to cache on first request

Quality presets:

Preset Resolution Bitrate
potato 480p ~500 Kbps
low 720p ~2 Mbps
medium 1080p ~5 Mbps
high original ~10 Mbps

Each quality level cached separately. Client can switch quality — mpv reloads the URL.

mpv on client:

video = http://server/api/stream/file.mp4?quality=low
audio = http://server/api/audio/file.mp4

mpv's --audio-file= flag plays both in sync with frame-accurate seeking.

API endpoints

Files

GET /api/files?root={root}
  → [{path, name, size, duration?, markers_count}]

GET /api/video/{path}
  → raw file with range requests

GET /api/stream/{path}?quality=low|medium|high|potato
  → cached transcoded video (no audio), range requests
  → 202 if transcode in progress

GET /api/audio/{path}
  → cached full-quality audio, range requests
  → 202 if extraction in progress

GET /api/cache/status/{path}
  → {qualities: {potato: "ready", low: "transcoding", ...}, audio: "ready"}

Markers & profiles

GET    /api/markers/{filename}?profile=default
  → [{start_time, marker_number, output_path}]

GET    /api/profiles
  → ["default", "intense", ...]

GET    /api/labels
  → ["dog barking", "rain", ...]

Export

POST   /api/export
  body: {input_path, cursor, folder_suffix?, name, clips, spread,
         short_side?, portrait_ratio?, crop_center, format,
         label?, category?, profile, crop_keyframes?,
         rand_portrait?, rand_square?, track_subject?}
  → {job_id}

GET    /api/export/{job_id}
  → {status, completed, total, outputs: [...]}

DELETE /api/export/{output_path}
  → delete from DB + disk

WS     /ws/export
  → server pushes: {type: "clip_done", path: "..."} | {type: "all_done"} | {type: "error", msg: "..."}

Hidden files

POST   /api/hidden/{filename}?profile=default
DELETE /api/hidden/{filename}?profile=default
GET    /api/hidden?profile=default
  → ["file1.mp4", "file2.mp4"]

Code reuse from main.py

Extracted to shared module (used by both server and Qt app):

  • ProcessedDB — SQLite operations
  • build_ffmpeg_command — ffmpeg command construction
  • build_audio_extract_command
  • build_export_path / build_sequence_dir
  • detect_hw_encoders
  • upsert_clip_annotation / remove_clip_annotation
  • apply_keyframes_to_jobs / resolve_keyframe
  • track_centers_for_jobs (subject tracking)

Server-specific (new):

  • FastAPI app + route handlers
  • Transcode cache manager
  • Export worker (plain threading, replaces QThread-based ExportWorker)
  • File listing / media root scanning
  • WebSocket export progress broadcaster

Tauri client (new, Svelte):

  • mpv integration via Tauri plugin or sidecar
  • Canvas-based timeline widget
  • Canvas-based crop overlay
  • All UI controls
  • API client module

Dockerfile

FROM python:3.12-slim
RUN apt-get update && apt-get install -y ffmpeg && rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY server/ .
RUN pip install --no-cache-dir fastapi uvicorn
EXPOSE 8000
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"]

Project structure

8-cut/
├── main.py              (existing Qt app, unchanged)
├── core/                (shared logic, extracted from main.py)
│   ├── __init__.py
│   ├── db.py            (ProcessedDB)
│   ├── ffmpeg.py        (build commands, detect encoders)
│   ├── export.py        (ExportWorker — plain threading)
│   ├── paths.py         (build_export_path, build_sequence_dir)
│   └── annotations.py   (dataset.json helpers)
├── server/
│   ├── app.py           (FastAPI app)
│   ├── routes/
│   │   ├── files.py
│   │   ├── stream.py
│   │   ├── markers.py
│   │   ├── export.py
│   │   └── hidden.py
│   ├── cache.py         (transcode cache manager)
│   ├── ws.py            (WebSocket handler)
│   └── config.py        (env vars, settings)
├── client/              (Tauri + Svelte — future)
│   └── ...
├── Dockerfile
└── docker-compose.yml

Implementation order

  1. Extract shared logic from main.py → core/
  2. Update main.py to import from core/ (verify Qt app still works)
  3. Build FastAPI server with file listing + video serving
  4. Add transcode cache + audio extraction
  5. Add markers/profiles/labels/hidden API
  6. Add export endpoint + WebSocket progress
  7. Dockerfile + docker-compose
  8. (Later) Tauri client