v0.9.13 · Resolve decades of scattered, copied, and duplicated photos into one organized directory structure. A local, browser-based tool — nothing leaves your machine, and originals are never modified or deleted.
(Formerly "Photo Copy Finder"; companion project to Objectif.AI.)
Point it at every folder your photos have ever accumulated in — phone
exports, Google Takeout, old backups of backups — and it indexes everything,
finds exact duplicates by content hash, resolves the date each file was
taken, flags near-duplicates (re-exports, filtered copies) for visual
review, and copies (never moves, never deletes) one clean set into a
Photos/YYYY/MM and Videos/YYYY/MM structure.
Everything runs locally on your machine. Nothing is uploaded anywhere.
Your original files are never modified or deleted. This tool only reads from your source folders and writes into the output folder you choose. Every phase is resumable — stop any time with the Stop button (or by closing the app) and re-run the step to continue where it left off.
If you don't already have it: download and install Python 3.11+ from https://www.python.org/downloads/windows/ — during install, check the box "Add python.exe to PATH."
Open Command Prompt in this folder and run:
pip install -r requirements.txt
python app.py
This starts a local server and opens http://127.0.0.1:5151 in your
browser automatically. Everything runs on your own machine — nothing is
uploaded anywhere.
First run on Windows: the helper tools install themselves. The app
depends on two standalone programs that aren't Python packages — ExifTool
(reads embedded photo/video dates; required for good results in the
date-resolution step) and FFmpeg (optional; video thumbnails). On first
launch the app checks for both and downloads the official builds into a
local tools\ folder next to app.py (~115 MB total, one time). Nothing is
installed system-wide, no admin rights are needed, and deleting tools\
removes them completely.
You can watch it happen: every step — connecting, download progress, success, or the exact error — streams to the in-page console (which opens automatically), the cmd window, and the two status pills in the page header (ExifTool / FFmpeg: green = ready, amber = downloading with a live %, red = missing or failed). Each download has a fallback mirror and automatic retries, and if a machine's TLS certificate store is broken (a stock-Windows Python quirk) the download recovers and says so in the log. The app is fully usable while downloads run.
If a download fails anyway (offline / firewall), the app still runs — click
a red pill (or ⬇ Download missing tools now in Help → Installing
ExifTool & FFmpeg) to retry, or follow the manual route in that same Help
section: drop exiftool.exe and ffmpeg.exe into tools\ and press the
button to re-check, no restart needed. On macOS/Linux use your package
manager (brew install exiftool ffmpeg /
sudo apt install libimage-exiftool-perl ffmpeg).
Without ExifTool, date resolution falls back to filename-parsing and filesystem dates for everything, which pushes far more files into review.
The interface deliberately stays terse — a short line per step. The full explanation of every section lives in the in-app Help & guide page: click ? Help & guide at the top of the main page, or the small ⓘ icon next to any section header to jump straight to that section's guide entry.
Follow the numbered steps on the page, in order:
-
Source directories — paste every folder that contains photos/videos anywhere inside it (one per line), one folder per box. Subfolders are scanned automatically. Click "Add directories," then "Run indexing." Alongside the indexed-by-type counts, an Ignored file types panel shows a census of every non-media file the scan walked past (
.jsonsidecars, documents, archives, system junk) so you can confirm the scan really saw your whole library. -
Exact duplicates — hashes every file and removes byte-identical copies. Safe, automatic, no review needed.
-
Resolve dates — resolves each file's date in this priority order: embedded metadata (EXIF / video container) → Google Takeout
.jsonsidecar capture time → a date pattern in the filename → filesystem date. Every file is tagged with the source and a confidence level. -
Near-duplicate scan — perceptual comparison across your entire photo library (not bucketed by date, folder, or name), so re-exports and filtered copies group with their original even when their dates disagree.
-
Review — opens a separate screen where you confirm any low/medium confidence dates and pick which file to keep from each near-duplicate group.
-
Verify (optional) — the Verification Tree at
/verify. One row per family: the file(s) that will be copied on the right with their exact destination path, and on the left every original that rolls up to them — the keeper in its source folder, skipped near-duplicates, and every byte-identical exact duplicate, all with real thumbnails, source paths, and open-in-Explorer. A reconciliation strip proves indexed = will-copy + skips + exact duplicates + errors. If you spot a mistake, mark any file KEEP right in the tree (undoable) without redoing earlier steps. Every keeper — standalone or one of several in a near-duplicate group — can also be removed from the plan (🗑, kept on disk, not copied) independently of the near-dup skip toggle. Made a mess of manual edits? ↺ Undo all manual edits on this page reverts every keep/skip/exclude/promotion back to what the scans computed, without re-running anything. Nothing on disk is touched. -
Copy — enter an output folder (e.g.
D:\SortedPhotos) and copy everything intoPhotos/YYYY/MMandVideos/YYYY/MM. Files with no resolved date land in aPhotos/UnsortedorVideos/Unsortedfolder instead of being guessed at. If a destination filename is already taken by a genuinely different file, the copy gets a_1/_2/... suffix; if the file already there is byte-identical (e.g. after resetting and re-running Copy), it's recognized and not re-copied. A summary line in the console reports both counts after the run — see the manifest for exactly which files were affected.If a file's date had to be assigned (Takeout JSON, filename pattern, filesystem mtime, or your manual edit in review), that date can also be embedded into the copy's metadata at 12:00 noon, so the organized library carries its dates into any photo app — the
.jsonsidecars are not copied, so this is how their capture dates survive. Each of the four sources is a checkbox on the copy step (all on by default). Files that already had a real embedded date are copied byte-identical, and source files are never modified. The manifest records which copies were stamped.
The 📁 Project picker at the top of the page lets you keep entirely
separate runs — different photo batches, different sources, whatever grouping
makes sense to you — each with its own index, review decisions, settings, and
console log. Everything lives under projects/<name>/ next to app.py.
- Click the picker to see every project with its file count and switch with
one click. + New project and Import snapshot… live in the picker's
footer; hovering any other project shows a 🗑 to delete it (deleting
only removes that project's tracking database, thumbnails, and log — it
never touches source photos or anything already copied to an output folder,
since neither ever lives inside
projects/). The currently-open project can't be deleted; switch away first. - ✎ Rename renames the current project.
- 💾 Snapshot copies the current project's database — checkpointed first, so it's always consistent — to a folder of your choice (defaults to the Copy step's output folder). Handy as a restore point once a copy run finishes, or before trying something you might want to undo.
- Import snapshot… (in the picker footer) restores a previously-saved
snapshot (or any
organizer.db) as a new project, so you can go back to an earlier state without disturbing whatever's currently open. - Upgrading from a version before projects existed (pre-0.9.9.1): your
existing
organizer.db(andThumbnails/) is automatically moved intoprojects/Default/the first time you launch — nothing is lost, it just becomes your first project.
Every step on the main page has a small ↺ Reset button next to its status chip. Resetting a step redoes it and every step after it in the pipeline — the mental model is deliberately simple: pick where you want to start over, and everything downstream comes along, no partial states to reason about. Before anything happens you get a confirmation listing exactly what will be discarded (manually-reviewed dates, reviewed near-dup group decisions, manual Verify-tree edits, copied-file tracking), and:
- Nothing on disk is ever touched by a reset — not source files, not copies already written to your output folder. Resetting Copy only clears the tracking flag, so a re-run recognizes files already correctly copied (see the filename-collision note in Step 7) instead of duplicating them.
- Resetting Copy alone doesn't need a full pipeline redo — it's the last step, so nothing downstream exists to cascade into.
- To undo just your manual edits in the Verification Tree without
resetting any computed step, use ↺ Undo all manual edits on
/verifyinstead — it's a lighter, non-destructive alternative to a full reset.
Confirming that two multi-GB videos are byte-identical means reading both in full, which is the slowest part of a big run. Step 2 has a setting: "Skip full-hashing videos larger than X GB" (0 = hash everything).
How it works when the cap is on:
- Every video still gets the instant 256KB head-hash, so a unique large video (nothing else shares its size + head) is still recognized and sorted into its normal date folder — no waiting.
- Only large videos that look like duplicates (matching size + first 256KB)
skip the slow full read. Instead of auto-deduping something unconfirmed,
every copy is kept and moved to
Videos/Unsorted_Large/for you to review by hand. Same-named copies are disambiguated asNAME-1.mp4,NAME-2.mp4, etc. (grouped for review — since they weren't fully hashed, they're likely but not proven identical).
This setting can be changed between runs and takes effect on resume — you can finish a stalled run by capping the giant videos without redoing the rest.
The ⚡ Auto Run button chains steps 1–4 — indexing, exact dedupe, date resolution, and the near-duplicate scan — back to back using your saved settings (worker threads, video size cap, match sensitivity). It deliberately stops before the copy step so you always get a chance to review dates and near-duplicate groups before anything is written. Each step shows a status chip (pending / running / done) and, once finished, its metrics: elapsed time, throughput, reclaimable space found, date sources, and more. Stopping an auto run is always safe — progress is saved and re-running resumes.
Five UI themes are available from the picker at the top of the page: Blueprint (default), Darkroom, Phosphor, Gallery (light), and Midnight. Your choice is remembered in the browser.
- A live date sidebar fills in year and month counts in real time while dates are being resolved, so you can watch your archive take shape.
- A Stop button appears while any phase runs. Stopping is always safe: progress is saved and re-running the step resumes where it left off.
- A live console (toggle at the top, and it opens automatically when a
phase starts) shows exactly what's happening and which file is being
processed right now. If a run ever seems stuck, the console tells you the
file it's on — usually a very large video or a cloud "online-only"
placeholder that's slow to read. The visible scrollback holds the last
5,000 lines, but that's just the live view — every line, for the whole
life of the project, is also written to
projects/<name>/run.logon disk, so nothing is ever truly lost to the buffer. Download it anytime with ⬇ Log in the project actions bar. - After indexing you get stats: photo count, video count, total size, and how many files were unreadable — so you can confirm the scan covered everything.
- After resolving dates, click "Show breakdown by year / month" for a histogram of how many photos and videos landed in each month.
Hashing and metadata reading are limited by your disk, not the CPU or GPU — so if you see low CPU usage during Step 2, that's expected; the drive is the bottleneck. A graphics card can't help here, so this tool doesn't pretend to use one. What actually matters:
- The quick-scan hashes only the first 256KB of each file, read sequentially — no seeking to the end of large files, which is what makes a spinning or network drive crawl. Only files that match on size and that head chunk get fully hashed.
- Exact-dedup is resumable. Every hash is saved to the database as it's computed, so if you stop the app (or it crashes) partway through a huge run, restarting Step 2 picks up where it left off instead of re-reading everything.
- Worker threads (control at the top of the page): on a single internal
SSD/NVMe, 8–16 threads is good. On a single spinning, USB, or mapped
network drive (e.g. a
Z:drive), fewer is usually faster — try 2–4, because many threads seeking at once thrash the disk head. - If your originals live on a slow external or network drive, the single biggest speed-up is copying them to a local SSD first, then pointing the tool at that. Confirming that two large videos are byte-identical means reading both in full, and that's inherently disk-bound wherever they live.
Note: the "unique" counter stays flat while a tier is hashing and jumps when the tier finishes — that's normal, not a freeze. Watch the progress bar and the live console to see it working.
- Nothing is ever moved or deleted. Every original file stays exactly where it is. The tool only ever copies into the output folder. Running it cannot erase a photo — the worst case is that a file doesn't make it into the sorted output, and the original is still on disk to recover.
- Every copy operation is logged to a
manifest_*.csvin the output folder: source path, destination, resolved date, date source, and whether it was copied or skipped. Review that manifest before you ever consider removing originals.
- The server binds to
127.0.0.1only — it is never reachable from other machines on your network, and requests that don't come from your own browser on this machine (spoofed Host headers, cross-site requests from other websites) are rejected outright. - Nothing is uploaded anywhere. The only network activity the app ever initiates is the one-time ExifTool/FFmpeg download on Windows, from their official sources, fully narrated in the console.
- Everything it learns about your library — paths, dates, hashes, thumbnails,
logs — stays in the local
projects/folder. Delete a project (or the whole folder) and that data is gone.
- Exact duplicates are decided by content, not size. Size is only a fast pre-filter; files that share a size get a head+tail hash, and files that still collide get a full SHA-256. Only a full-content match is ever called a duplicate — two different files that happen to share a size are both kept.
- Near-duplicate detection spans the whole library. It uses perceptual hashing plus a BK-tree neighbour search, so it finds look-alikes regardless of date, folder, or filename — the case where a Google re-export sits in a different month than the iPhone original is handled. Perceptual hashes are stored in the database, so this step resumes rather than recomputing if interrupted.
- The pipeline is resumable throughout — it's all backed by a SQLite
database (
organizer.db, underprojects/<name>/next toapp.py). Close it or crash mid-run, restart, and re-run the current step. - Near-duplicate sensitivity is selectable (strict / balanced / loose; balanced by default). Every file in a group is kept by default — nothing is skipped from the sorted output unless you uncheck it in the review screen. Each thumbnail shows its file size so you can spot the best copy when deciding what to skip.
- HEIC (iPhone) support requires
pillow-heif, already inrequirements.txt. - Video near-duplicate detection isn't included — true perceptual video matching is a much bigger undertaking. Exact-duplicate video detection (step 2) works fully.
Three options, from lightest to heaviest:
- Redo one step (and everything after it) — click ↺ Reset on that step's card. See Resetting a step above.
- Start a clean project without losing the old one — + New project in the Project bar. Your old project (and its database) is still there; switch back to it anytime.
- Wipe everything in the current project — delete its folder under
projects/<name>/and restart the app, or just use ↺ Reset on Step 1 (Source directories), which clears the same data without a restart.
Your source folders and anything already copied to an output folder are untouched by any of the above — all three only ever affect the tracking database.