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>
configuredevShellbackendbackend-devcheck-bashcheck-frontendcheck-go-formattingcheck-go-test-packagescheck-trailing-newlinecheck-trailing-whitespacedeploy-to-flydiagramdocker-imagee2e-testsgo-testsimport-from-tinybeanslint-sqllittle-momentslittle-moments-devx86_64-linux