Replaced-file re-index: threshold hash comparison and upload dedup after re-key
Summary
Two follow-ups from the #2039 review (fixes #730), both non-destructive today but worth closing.
1. Compare perceptual hashes with a threshold, not equality
_picture_verdict in apps/backend/api/directory_watcher/file_handlers.py treats a file as a new picture when the perceptual hash differs at all. An external tool that re-encodes the image (lossy recompress, EXIF rewrite that re-saves pixels) changes the hash slightly with no visible change, which currently costs the photo its face labels. api.perceptual_hash.are_duplicates(a, b, threshold) already exists; use it with a small Hamming distance so near-identical content is bookkeeping only.
2. Photo.image_hash and main_file.hash diverge after a same-picture re-key
In the SAME_PICTURE branch only the File row is re-keyed. That is intended, but UploadPhotosChunkedComplete.target_path dedupes uploads on image_hash, so re-uploading that exact file is no longer recognised as a duplicate and creates a second Photo. Either dedupe uploads on the content part of File.hash as well, or document that image_hash is a stable identity and not the current content hash.
Source: LibrePhotos/librephotos