By: Michael Lynch <git@mtlynch.io>
Unify media converters behind one stream-based interface ProcessVideo diverged from ProcessPhoto/ProcessAnimatedPhoto: video conversion was injected as a path-based func(ctx, srcPath, dstPath) while photos used stream-based func(io.Reader, io.Writer). Give all three the same shape so video is no longer a special case. Add a Converter type, func(ctx, io.Reader, io.Writer) error, used by all three MediaConverter fields. ctx keeps slow video (and now GIF) transcodes cancellable on request disconnect or shutdown. ProcessVideo now writes its full-size variant through the same writeFullSize path as photos; the video-specific duration probe and frame extraction stay path-based since they have no photo analog. To avoid re-copying a large uploaded video through an extra temp file, writeConvertedFile reuses the source file's path directly when the reader is already a regular file on disk (every upload), falling back to a temp-file copy otherwise. Export the photo converter as convert.PhotoToJPEG rather than defining it inline at the composition root, matching the other two converters. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>