73dd7a1569
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
6.5 KiB
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:
- Client requests
/api/stream/{path}?quality=low - Server checks cache:
{CACHE_DIR}/{quality}/{hash}.mp4 - If cached → serve with range requests (instant seeking)
- If not → start background ffmpeg transcode, return
202 Acceptedwith job ID - Client polls or gets WebSocket notification when ready
- 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 operationsbuild_ffmpeg_command— ffmpeg command constructionbuild_audio_extract_commandbuild_export_path/build_sequence_dirdetect_hw_encodersupsert_clip_annotation/remove_clip_annotationapply_keyframes_to_jobs/resolve_keyframetrack_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
- Extract shared logic from main.py →
core/ - Update main.py to import from
core/(verify Qt app still works) - Build FastAPI server with file listing + video serving
- Add transcode cache + audio extraction
- Add markers/profiles/labels/hidden API
- Add export endpoint + WebSocket progress
- Dockerfile + docker-compose
- (Later) Tauri client