A lathe takes raw stock and turns it down to a target spec. That is the job here — media, resized and re-encoded entirely on the device, with no subprocesses and no server round-trip.
Early. Probing, loudness, frame extraction and the capability probe are implemented and tested. The encoders are not written yet — every unimplemented entry point throws a named error rather than crashing or silently succeeding. This page says what is true today, not what is planned.
Most media tooling on Apple platforms is a command-line binary wrapped in a process spawn. That is fine on a desktop and impossible on iOS, where fork/exec is not available at all.
The usual answer is to port the C tools. That answer is mostly wrong: the system frameworks already do most of the work, in hardware, with no licence exposure. ImageIO encodes HEIC, AVIF, JPEG, PNG, GIF, TIFF, JPEG 2000 and PDF. AVFoundation probes and extracts frames. VideoToolbox encodes H.264 and HEVC with constant-quality control. Vision does OCR.
Lathe is the thin, well-tested layer over those frameworks — plus a small number of permissively licensed native libraries for the gaps they genuinely leave.
Container headers through AVURLAsset — duration, per-track codec, coded and display dimensions, frame rate, bit rate, sample rate, channels. No decoding, so probing a four-hour film costs what probing a four-second clip costs.
The same facts re-emitted in the shape ffprobe -of json produces, so a subprocess prober can be replaced as one reviewable change. The numeric-looking fields stay strings, because every parser written against ffprobe expects them to be.
Two questions that are easy to mistake for one. A measurement, and a decision. See below — the difference is the interesting part.
LoudnessProbeThumbnails at a requested time and max edge, written through CGImageDestination; and raw 8-bit grayscale frames for perceptual hashing, which never touch disk.
Asks the OS what it can actually encode by attempting a destination, not by reading a type list. Correct on OS versions nobody has tested, which matters because some identifiers carry no availability annotation at all.
EncodeSupportOne mechanism for both: the progress callback's return value is the cancellation signal, so there is no second channel to forget to check.
ProgressHandleMean volume over a whole file is the standard way to ask "does this have sound", and it is quietly wrong. Ten minutes containing two seconds of speech averages about −45 dBFS — indistinguishable, by that number, from a file with nothing in it at all.
That is not a corner case. It is an ordinary phone video with one spoken sentence in it.
let volume = try await LoudnessProbe().meanVolumeDB(url: url) // a measurement
let audible = try await LoudnessProbe().hasAudibleAudio(url: url) // a decision
meanVolumeDB reports the same statistic the familiar command-line filter prints, so existing thresholds keep working — but new logic should use hasAudibleAudio, which reads windowed peak and short-term RMS and stops as soon as it has an answer. A loud file is decided in the first window; only a genuinely silent one is read to the end, because that is the only way to know.
Lathe is one repository with several products, selected individually. That split is not cosmetic — it is how an App Store target stays clear of code it should not contain.
| Product | Contents | App Store |
|---|---|---|
| Lathe | The media-processing modules, together | always safe |
| LatheCore | Shared types, progress, errors | safe |
| LatheImage | Still images, capability probe | safe |
| LatheVideo | Probing, frames, transcode | safe |
| LatheDoc | PDF and archives | safe |
| LatheAudio | Loudness and audibility | safe |
| network ingest | Fetching from the network — not yet written | excluded from the umbrella |
The umbrella product is defined as the media-processing modules and will never absorb a network-ingest module. Depending on Lathe therefore cannot pull one in by accident; linking one has to be an explicit line in your own manifest.
.package(url: "https://github.com/blaineam/Lathe.git", from: "0.1.0")
// then, per target — take only what you need:
.product(name: "LatheVideo", package: "Lathe")
This section will hold before/after comparisons with a draggable slider, real byte counts, real percentages saved, and a quality score — each one naming the tool it is measured against, because "40% smaller" means nothing without "…than what".
Those figures will be generated by the test suite and regenerated on every release, not written by hand. Until the encoders exist there is nothing to generate, and publishing numbers that nothing produced is exactly what that design is meant to prevent.