Skip to content

cocktailpeanut/cropper

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Cropper

Cropper is a local web app for:

  • video crop + trim (manual drag box + presets, timeline trim)
  • image crop (manual drag box + presets, heuristic auto-crop)
  • audio trim (single range or multi-segment cuts)
  • auto-crop detection for video/image
    • black bars (static or adaptive)
    • subject/activity region (static or adaptive for video)

The app runs fully local with a FastAPI backend and ffmpeg processing.

Project Structure

cropper.git/
├── app/
│   ├── main.py
│   ├── requirements.txt
│   ├── static/
│   │   ├── index.html
│   │   ├── app.css
│   │   └── app.js
│   └── tmp/
├── install.js
├── start.js
├── update.js
├── reset.js
├── pinokio.js
├── pinokio.json
└── README.md

How To Use (Pinokio)

  1. Click Install.
  2. Click Start.
  3. Open Open Web UI.
  4. Upload media (video, image, or audio).
  5. Set crop/trim/segments/auto-crop as applicable.
  6. Click Process Media and download the result.

Input/Output Format Notes

  • Input: Any video/audio format your local ffmpeg can decode; common image formats (png/jpg/webp). GIF is not supported.
  • Output defaults: MP4 (H.264/AAC) for video, PNG for image, MP3 for audio. You can change formats per media.
  • Storage: Uploads and outputs are temporary and auto-cleaned.

Local Development (without Pinokio)

cd app
uv venv env
source env/bin/activate
uv pip install -r requirements.txt
uvicorn main:app --host 127.0.0.1 --port 8000

API

1. Upload Media

POST /api/upload (multipart form field: file)

Response includes:

  • media_id
  • media_type (video, image, audio)
  • preview_url
  • metadata
    • video: width, height, duration, frame_rate, has_audio, codec
    • image: width, height
    • audio: duration, sample_rate, channels, codec

2. Auto-Crop Suggestion (Video/Image)

POST /api/media/{media_id}/autocrop

Body:

{
  "mode": "black_static",
  "window_seconds": 8,
  "aspect_ratio": "free"
}

Modes:

  • black_static
  • black_adaptive
  • subject_static
  • subject_adaptive

Returns either:

  • kind: "static" with crop
  • kind: "adaptive" with crop and adaptive_plan Notes:
  • Adaptive plans are only generated for video; images always return a static crop.
  • Backwards-compatible alias: POST /api/videos/{video_id}/autocrop.

3. Process Media (Async with Progress)

Start job: POST /api/process/start

Body:

{
  "media_id": "<id>",
  "output_format": "mp4",
  "trim_start": 0,
  "trim_end": 22.5,
  "segments": [
    { "start": 0, "end": 5.2 },
    { "start": 8.0, "end": 14.5 }
  ],
  "crop": { "x": 20, "y": 10, "width": 1280, "height": 720 },
  "adaptive_plan": null
}

Notes:

  • If segments is non-empty, it overrides single trim range.
  • If adaptive_plan is provided, backend applies adaptive crop first, then trim/cut (video only).
  • Audio ignores crop/adaptive_plan; images ignore trim/segments.

Poll job:

GET /api/process/jobs/{job_id}

Returns status, progress (0-100), stage, and result on completion.

4. Process Media (Blocking Compatibility)

POST /api/process

Same payload as above. This endpoint blocks until finished.

5. Fetch Result

  • Preview: GET /api/outputs/{output_id}/stream
  • Download: GET /api/outputs/{output_id}/download
  • Upload preview: GET /api/media/{media_id}/stream (alias: /api/videos/{video_id}/stream)
  • Audio waveform: GET /api/media/{media_id}/waveform

Programmatic Examples

JavaScript

const upload = new FormData();
upload.append("file", fileInput.files[0]);
const uploaded = await fetch("/api/upload", { method: "POST", body: upload }).then(r => r.json());

const auto = await fetch(`/api/media/${uploaded.media_id}/autocrop`, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ mode: "black_static", window_seconds: 8, aspect_ratio: "16:9" })
}).then(r => r.json());

const started = await fetch("/api/process/start", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    media_id: uploaded.media_id,
    output_format: "mp4",
    trim_start: 0,
    trim_end: 20,
    segments: [],
    crop: auto.crop,
    adaptive_plan: auto.kind === "adaptive" ? auto.adaptive_plan : null
  })
}).then(r => r.json());

let final;
while (!final) {
  const job = await fetch(`/api/process/jobs/${started.job_id}`).then(r => r.json());
  if (job.status === "error") throw new Error(job.error || "process failed");
  if (job.status === "completed") final = job.result;
  await new Promise(r => setTimeout(r, 500));
}

console.log(final.download_url);

Python

import requests

with open("input.mp4", "rb") as f:
    uploaded = requests.post("http://127.0.0.1:8000/api/upload", files={"file": f}).json()

auto = requests.post(
    f"http://127.0.0.1:8000/api/media/{uploaded['media_id']}/autocrop",
    json={"mode": "black_static", "window_seconds": 8, "aspect_ratio": "16:9"},
).json()

payload = {
    "media_id": uploaded["media_id"],
    "output_format": "mp4",
    "trim_start": 0,
    "trim_end": 20,
    "segments": [],
    "crop": auto["crop"],
    "adaptive_plan": auto.get("adaptive_plan") if auto.get("kind") == "adaptive" else None,
}

started = requests.post("http://127.0.0.1:8000/api/process/start", json=payload).json()
job_id = started["job_id"]
while True:
    job = requests.get(f"http://127.0.0.1:8000/api/process/jobs/{job_id}").json()
    if job["status"] == "error":
        raise RuntimeError(job.get("error") or "process failed")
    if job["status"] == "completed":
        result = job["result"]
        break
print("Download:", f"http://127.0.0.1:8000{result['download_url']}")

curl

# 1) Upload
curl -s -X POST -F "[email protected]" http://127.0.0.1:8000/api/upload

# 2) Auto-crop (replace MEDIA_ID)
curl -s -X POST \
  -H "Content-Type: application/json" \
  -d '{"mode":"black_static","window_seconds":8,"aspect_ratio":"16:9"}' \
  http://127.0.0.1:8000/api/media/MEDIA_ID/autocrop

# 3) Start process job (replace MEDIA_ID and crop values)
curl -s -X POST \
  -H "Content-Type: application/json" \
  -d '{
    "media_id":"MEDIA_ID",
    "output_format":"mp4",
    "trim_start":0,
    "trim_end":20,
    "segments":[],
    "crop":{"x":0,"y":60,"width":1920,"height":960},
    "adaptive_plan":null
  }' \
  http://127.0.0.1:8000/api/process/start

# 4) Poll job (replace JOB_ID)
curl -s http://127.0.0.1:8000/api/process/jobs/JOB_ID

About

crop anything

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages