Getting started

This page takes you from nothing to a trimmed clip and a lipsynced video. It uses curl and jq on the host and the CPU image, which is enough for every ffmpeg operation.

1. Start the container

docker run -d --name flickies \
  -v "$HOME/flickies-data:/data" \
  -p 127.0.0.1:8000:8000 \
  psyb0t/flickies:latest

The container serves REST and MCP on port 8000. Everything it keeps (staged files, model weights, logs) lands in $HOME/flickies-data on the host. Check that it is up:

curl -s http://localhost:8000/healthz
{
  "status": "ok"
}

GET /v1/health gives more: version, device, ffmpeg version, the engines in the registry and which one is loaded.

curl -s http://localhost:8000/v1/health | jq

2. Upload a clip

Every video endpoint reads its input either from the staging area (file_path) or from a URL the server downloads (file_url). The staging area is /data/files inside the container, so $HOME/flickies-data/files on the host. Upload with a raw-body PUT:

curl -s -X PUT \
  --data-binary @clip.mp4 \
  http://localhost:8000/v1/files/uploads/clip.mp4 | jq
{
  "path": "uploads/clip.mp4",
  "size": 1048576,
  "sha256": "..."
}

Copying the file into $HOME/flickies-data/files/uploads/ on the host works too.

3. Probe it

curl -s -X POST http://localhost:8000/v1/video/info \
  -H "Content-Type: application/json" \
  -d '{"file_path": "uploads/clip.mp4"}' | jq

The answer carries duration_sec, width, height, fps, video_codec, audio_codec, bitrate, container_format and size_bytes.

4. Trim it

A video-producing endpoint needs exactly one output: output_path (written into the staging area) or output_url (the server PUTs the result there).

curl -s -X POST http://localhost:8000/v1/video/trim \
  -H "Content-Type: application/json" \
  -d '{
    "file_path": "uploads/clip.mp4",
    "start_sec": 2,
    "end_sec": 7,
    "precise": true,
    "output_path": "out/clip-trimmed.mp4"
  }' | jq
{
  "path": "out/clip-trimmed.mp4",
  "size": 734003
}

Download it:

curl -s -o clip-trimmed.mp4 \
  http://localhost:8000/v1/files/out/clip-trimmed.mp4

precise: true re-encodes for frame-accurate cuts. Without it, trim copies streams and the start snaps to a keyframe. See REST API.

5. Lipsync a face to audio

Lipsync needs a face (video or still image) and a driving audio track. On the CPU image the only lipsync engines are Wav2Lip and Wav2Lip-GAN, and they are trained on non-commercial data, so the server refuses them unless you opt in. Restart the container with the opt-in:

docker rm -f flickies
docker run -d --name flickies \
  -e FLICKIES_ENABLE_NONCOMMERCIAL=1 \
  -v "$HOME/flickies-data:/data" \
  -p 127.0.0.1:8000:8000 \
  psyb0t/flickies:latest

On a GPU host, use psyb0t/flickies:latest-cuda with --gpus all instead and leave the opt-in out: the default engine, latentsync-1.5, is Apache-2.0. Read Engines before you choose.

Upload the face and the audio, then submit the job with async_job, since lipsync can take minutes:

curl -s -X PUT --data-binary @face.mp4 \
  http://localhost:8000/v1/files/uploads/face.mp4

curl -s -X PUT --data-binary @voice.wav \
  http://localhost:8000/v1/files/uploads/voice.wav

curl -s -X POST http://localhost:8000/v1/video/lipsync \
  -H "Content-Type: application/json" \
  -d '{
    "face_path": "uploads/face.mp4",
    "audio_path": "uploads/voice.wav",
    "engine": "wav2lip",
    "async_job": true
  }' | jq
{
  "job_id": "3f0c...",
  "status": "accepted"
}

Poll the job until status is complete or failed:

curl -s http://localhost:8000/v1/jobs/3f0c... | jq
{
  "job_id": "3f0c...",
  "status": "complete",
  "result": {
    "path": "jobs/9b1e....mp4",
    "size": 2211840
  },
  "error": null
}

Download result.path from /v1/files/. The file name under jobs/ is not the job id, so always read it from result.path.

The first lipsync request downloads the engine’s Hugging Face repository into /data/hf, which takes a while. The request that triggers the download waits for it even with async_job, because the engine loads before the job is queued. To download at boot instead, see Deployment.

Next