Convert Audio, Extract Audio, Convert Video. ported from the versions that ran on
alpha for months, which is where the rules come from — they are not invented:
skip a file already in the target format (re-encoding mp3 to mp3 loses quality
for nothing)
never overwrite an existing output (the file there may be one somebody made)
delete the source only AFTER the output exists, and only when asked
`inline: ask`, so the modal offers running it now or as a job — a few files inline,
a discography as a job. that was the affordance the old ones had and the reason the
choice exists.
THIN CLIENTS. every one calls /v1/ffmpeg; none of them runs ffmpeg. what to tell
ffmpeg lives once in av-club rather than three times in bash, which is the whole
point of the plugin existing. the three files nevertheless repeat their walk and
their reporting, because the task runner materialises ONLY index.ts and a script
task cannot import a sibling — that duplication is the platform's shape, not a
choice, and each file says so.
CONVERT VIDEO PROBES BEFORE IT DECIDES. if the codecs already fit the container it
asks for `c:v copy, c:a copy` — a remux, lossless, roughly a hundred times faster
than re-encoding. guessing from the extension is how a "fast remux" silently
re-encodes, since plenty of .avi holds h264. that needed a path probe, so
/v1/probe?path= is added beside it, same containment, read-only.
EXTRACT AUDIO HAS NO delete_source, deliberately. extracting is additive; offering
to delete would be offering to trade a video for its soundtrack. a video with no
audio track is reported as a skip rather than a failure — in a folder of screen
recordings that is most of them.
what is NOT here: track selection, subtitle handling, downmixing, per-file
percentage. those belong with jellyfin rather than with basic conversion and are
left where they are rather than half-ported. convert-video's TASK.md says so.
VERIFIED by running each task the way the runner does — temp dir, env vars, argv:
flac→mp3 on a folder; the re-run skipping both files for the two different reasons;
extraction skipping the silent clip and converting the other; mp4→mkv copying h264
and aac through untouched; mp4→webm re-encoding to vp9/opus; delete_source removing
a source after success and LEAVING it after a failure.
the skill tells an agent to use the endpoint and never to shell out to ffmpeg —
everything that makes this safe is on the far side of it — and to offer the
right-click tasks first when somebody is at a screen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
av-club
Turning whatever someone uploads into assets a website can actually serve.
People upload photos and videos straight off a phone or a camera: a 12 MB HEIC, a 4K clip in a codec Safari refuses to play, a JPEG carrying the GPS coordinates of the room it was shot in. None of that belongs on a page as-is. This is the thing in between.
It is an HTTP service that exposes as much of sharp, ffmpeg and ffprobe as can be exposed safely, keeping each tool's own vocabulary — sharp operations are named what sharp names them, ffprobe fields come back spelled the way ffprobe spells them. If you know the tool, you know the API.
What it will not let you do is name a file. That single restriction is what makes the rest safe to
open up; see docs/SECURITY.md.
Status
Assets, probing and the full sharp surface work. ffmpeg is designed but not built — that is the next piece.
docs/API.md |
The HTTP surface — start here if you are calling it |
docs/SHARP.md |
Every sharp operation and its bounds. Generated from the validator, so it cannot drift |
docs/SECURITY.md |
The threat model and the invariants. Read before adding an endpoint |
docs/TOOLING.md |
What is installed, and the two things that bite |
docs/PIPELINE.md |
What this is for, and what is still undecided |
Quick start
./scripts/install.sh # system packages + bun deps, then verifies
cp .env.example .env # then set AV_CLUB_TOKEN
bun dev
Verified on Ubuntu 24.04. The installer says so rather than guessing if it finds something else.
# upload something, then resize it
curl -X POST localhost:3020/v1/assets -H "Authorization: Bearer $AV_CLUB_TOKEN" -F file=@photo.jpg
curl -X POST localhost:3020/v1/sharp -H "Authorization: Bearer $AV_CLUB_TOKEN" \
-H 'Content-Type: application/json' -d '{
"input": "as_…",
"pipeline": [{ "rotate": {} }, { "resize": { "width": 1200, "withoutEnlargement": true } }],
"output": { "format": "avif", "options": { "quality": 55 } }
}'
bun run verify checks the tooling; bun run docs regenerates docs/SHARP.md from the validator.
Why this is its own repo
It has system dependencies a web app should not have to assume. ffmpeg, ImageMagick, libvips and
exiftool are a real installation, not an npm install. Keeping them here means a consuming project
can state that it needs this, rather than quietly failing on a machine that happens not to have
ffprobe on its PATH.
It changes on a different clock. Encoder settings, derivative sizes and format choices get revisited as browser support moves. Page templates and database schemas do not.
Nothing about it is specific to one site. Resizing an image and extracting a poster frame are the same problem everywhere, which is why this is not buried inside the project that first needed it.
Its first consumer
fofos/platform — a website with a public site, an admin, and a database. Admins upload media
through the admin; this processes it; the platform stores what comes back.
How that handoff works is not decided yet. The honest options are a library the platform imports,
a CLI it shells out to, or a service it talks to. Each has real consequences for deployment and for
whether processing blocks an admin's upload while they wait. That question is written down in
docs/PIPELINE.md as open, rather than being answered by whatever happens to get built first.
Working agreements
- Commit and push every turn that changes anything — working, broken, or half-finished. The history is the artifact; it is how decisions stay traceable and mistakes stay learnable.
- Docs are live documents, and the code is the source of truth. Docs move with the code in the same commit. A doc contradicting the code is a bug in the doc.
- Direction changes fast, and that is expected. Nothing written here is frozen.