9954a15e

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>

Suite timing

Time to Start Worker time Duration Time to finish
Config 2s 1m34s 1m34s 1m36s
Eval 1m35s 2m24s 2m24s 4m00s
Build 1m57s 8m56s 3m40s 5m38s
Suite 2s 12m55s 5m35s 5m38s

Timeline

0s1m2m3m4m