photo-stack-finder 0.1.7__tar.gz → 0.1.8__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/PKG-INFO +2 -3
- {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/pyproject.toml +2 -3
- {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/orchestrator/__init__.py +2 -2
- {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/orchestrator/app.py +6 -11
- {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/orchestrator/build_pipeline.py +19 -21
- {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/orchestrator/orchestrator_runner.py +11 -8
- {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/orchestrator/static/orchestrator.html +6 -0
- {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/orchestrator/static/orchestrator.js +2 -0
- {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/overlap_metrics/__init__.py +1 -1
- {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/photo_compare/__init__.py +1 -1
- {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/photo_stack_finder.egg-info/PKG-INFO +2 -3
- {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/photo_stack_finder.egg-info/SOURCES.txt +1 -2
- {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/scripts/orchestrate.py +11 -9
- {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/utils/__init__.py +4 -3
- {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/utils/comparison_gates.py +40 -21
- {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/utils/compute_identical.py +92 -22
- {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/utils/compute_sha_bins.py +62 -20
- {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/utils/config.py +8 -5
- {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/utils/logger.py +2 -2
- {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/utils/models.py +2 -2
- {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/utils/photo_file.py +90 -91
- {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/utils/review_utils.py +10 -19
- {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/utils/sequence.py +10 -8
- {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/utils/sequence_clustering.py +1 -1
- photo_stack_finder-0.1.8/src/utils/template_parsing.py +71 -0
- {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/tests/test_compute_identical_comprehensive.py +36 -20
- {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/tests/test_compute_indices_comprehensive.py +8 -0
- {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/tests/test_compute_perceptual_hash_comprehensive.py +8 -0
- {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/tests/test_template_similarity_comprehensive.py +8 -0
- photo_stack_finder-0.1.7/src/utils/compute_templates.py +0 -137
- photo_stack_finder-0.1.7/tests/test_compute_templates_comprehensive.py +0 -386
- {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/LICENSE +0 -0
- {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/README.md +0 -0
- {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/setup.cfg +0 -0
- {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/orchestrator/pipeline_builder.py +0 -0
- {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/orchestrator/pipeline_orchestrator.py +0 -0
- {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/orchestrator/py.typed +0 -0
- {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/orchestrator/review_persistence.py +0 -0
- {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/orchestrator/static/favicon.svg +0 -0
- {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/orchestrator/static/orchestrator.css +0 -0
- {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/orchestrator/static/review_common.js +0 -0
- {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/orchestrator/static/review_identical.html +0 -0
- {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/orchestrator/static/review_sequences.html +0 -0
- {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/overlap_metrics/config.py +0 -0
- {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/overlap_metrics/core.py +0 -0
- {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/overlap_metrics/estimators.py +0 -0
- {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/overlap_metrics/metrics.py +0 -0
- {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/overlap_metrics/registry.py +0 -0
- {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/overlap_metrics/utils.py +0 -0
- {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/photo_compare/base.py +0 -0
- {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/photo_compare/config.py +0 -0
- {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/photo_compare/distance.py +0 -0
- {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/photo_compare/feature_methods.py +0 -0
- {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/photo_compare/file_hash.py +0 -0
- {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/photo_compare/hash_methods.py +0 -0
- {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/photo_compare/histogram_methods.py +0 -0
- {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/photo_compare/pixel_methods.py +0 -0
- {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/photo_compare/py.typed +0 -0
- {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/photo_compare/structural_methods.py +0 -0
- {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/photo_compare/types.py +0 -0
- {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/photo_stack_finder.egg-info/dependency_links.txt +0 -0
- {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/photo_stack_finder.egg-info/entry_points.txt +0 -0
- {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/photo_stack_finder.egg-info/requires.txt +0 -0
- {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/photo_stack_finder.egg-info/top_level.txt +0 -0
- {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/utils/base_pipeline_stage.py +0 -0
- {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/utils/base_ports.py +0 -0
- {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/utils/benchmark_utils.py +0 -0
- {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/utils/channel.py +0 -0
- {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/utils/compute_benchmarks.py +0 -0
- {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/utils/compute_indices.py +0 -0
- {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/utils/compute_perceptual_hash.py +0 -0
- {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/utils/compute_perceptual_match.py +0 -0
- {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/utils/compute_template_similarity.py +0 -0
- {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/utils/compute_versions.py +0 -0
- {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/utils/data_io.py +0 -0
- {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/utils/graph_context.py +0 -0
- {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/utils/pipeline_graph.py +0 -0
- {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/utils/pipeline_stage.py +0 -0
- {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/utils/plot_helpers.py +0 -0
- {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/utils/ports.py +0 -0
- {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/utils/progress.py +0 -0
- {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/utils/py.typed +0 -0
- {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/utils/report_builder.py +0 -0
- {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/utils/review_types.py +0 -0
- {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/utils/template.py +0 -0
- {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/tests/test_app_endpoints_comprehensive.py +0 -0
- {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/tests/test_comparison_methods_comprehensive.py +0 -0
- {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/tests/test_compute_benchmarks_comprehensive.py +0 -0
- {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/tests/test_compute_benchmarks_design.py +0 -0
- {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/tests/test_compute_perceptual_match_comprehensive.py +0 -0
- {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/tests/test_compute_sha_bins_comprehensive.py +0 -0
- {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/tests/test_compute_versions_comprehensive.py +0 -0
- {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/tests/test_orchestrator_comprehensive.py +0 -0
- {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/tests/test_photo_file_pixel_array.py +0 -0
- {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/tests/test_rotation_aware_comparison.py +0 -0
- {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/tests/test_server_shutdown.py +0 -0
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: photo-stack-finder
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.8
|
|
4
4
|
Summary: Photo deduplication using perceptual hashing and sequence detection
|
|
5
5
|
Author: Geoff Barrett
|
|
6
6
|
Maintainer: Geoff Barrett
|
|
7
|
-
License: AGPL-3.0-or-later
|
|
7
|
+
License-Expression: AGPL-3.0-or-later
|
|
8
8
|
Project-URL: Homepage, https://github.com/gbarrett28/photo_dedup
|
|
9
9
|
Project-URL: Repository, https://github.com/gbarrett28/photo_dedup
|
|
10
10
|
Project-URL: Issues, https://github.com/gbarrett28/photo_dedup/issues
|
|
@@ -12,7 +12,6 @@ Project-URL: Discussions, https://github.com/gbarrett28/photo_dedup/discussions
|
|
|
12
12
|
Keywords: photo,deduplication,perceptual-hashing,image-processing
|
|
13
13
|
Classifier: Development Status :: 4 - Beta
|
|
14
14
|
Classifier: Intended Audience :: End Users/Desktop
|
|
15
|
-
Classifier: License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)
|
|
16
15
|
Classifier: Programming Language :: Python :: 3
|
|
17
16
|
Classifier: Programming Language :: Python :: 3.11
|
|
18
17
|
Classifier: Programming Language :: Python :: 3.12
|
|
@@ -7,11 +7,11 @@ orchestrator = ["static/*", "py.typed"]
|
|
|
7
7
|
|
|
8
8
|
[project]
|
|
9
9
|
name = "photo-stack-finder"
|
|
10
|
-
version = "0.1.
|
|
10
|
+
version = "0.1.8"
|
|
11
11
|
description = "Photo deduplication using perceptual hashing and sequence detection"
|
|
12
12
|
readme = "README.md"
|
|
13
13
|
requires-python = ">=3.11"
|
|
14
|
-
license =
|
|
14
|
+
license = "AGPL-3.0-or-later"
|
|
15
15
|
authors = [
|
|
16
16
|
{name = "Geoff Barrett"}
|
|
17
17
|
]
|
|
@@ -22,7 +22,6 @@ keywords = ["photo", "deduplication", "perceptual-hashing", "image-processing"]
|
|
|
22
22
|
classifiers = [
|
|
23
23
|
"Development Status :: 4 - Beta",
|
|
24
24
|
"Intended Audience :: End Users/Desktop",
|
|
25
|
-
"License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)",
|
|
26
25
|
"Programming Language :: Python :: 3",
|
|
27
26
|
"Programming Language :: Python :: 3.11",
|
|
28
27
|
"Programming Language :: Python :: 3.12",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
"""Orchestration layer for photo
|
|
1
|
+
"""Orchestration layer for photo stack finding workflow.
|
|
2
2
|
|
|
3
3
|
This module provides the web-based interface and pipeline orchestration for the
|
|
4
|
-
photo
|
|
4
|
+
photo stack finding system. It includes:
|
|
5
5
|
|
|
6
6
|
- FastAPI web application (app)
|
|
7
7
|
- Pipeline orchestration and execution (PipelineOrchestrator)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""FastAPI orchestration server for photo
|
|
1
|
+
"""FastAPI orchestration server for photo stack finding.
|
|
2
2
|
|
|
3
3
|
This provides a web-based interface for configuring and running the pipeline.
|
|
4
4
|
|
|
@@ -209,7 +209,7 @@ async def lifespan_manager(app: FastAPI) -> AsyncIterator[None]:
|
|
|
209
209
|
|
|
210
210
|
|
|
211
211
|
# Pass the new lifespan function to the FastAPI constructor
|
|
212
|
-
app = FastAPI(title="Photo
|
|
212
|
+
app = FastAPI(title="Photo Stack Finder Orchestrator", lifespan=lifespan_manager)
|
|
213
213
|
|
|
214
214
|
|
|
215
215
|
@app.get("/")
|
|
@@ -876,9 +876,7 @@ async def websocket_progress(
|
|
|
876
876
|
async def serve_review_identical() -> FileResponse:
|
|
877
877
|
"""Serve identical files review interface."""
|
|
878
878
|
static_path = (
|
|
879
|
-
Path(CONFIG.orchestrator.STATIC_DIR)
|
|
880
|
-
if CONFIG.orchestrator.STATIC_DIR
|
|
881
|
-
else Path(__file__).parent / "static"
|
|
879
|
+
Path(CONFIG.orchestrator.STATIC_DIR) if CONFIG.orchestrator.STATIC_DIR else Path(__file__).parent / "static"
|
|
882
880
|
)
|
|
883
881
|
return FileResponse(static_path / "review_identical.html")
|
|
884
882
|
|
|
@@ -887,9 +885,7 @@ async def serve_review_identical() -> FileResponse:
|
|
|
887
885
|
async def serve_review_sequences() -> FileResponse:
|
|
888
886
|
"""Serve sequences review interface."""
|
|
889
887
|
static_path = (
|
|
890
|
-
Path(CONFIG.orchestrator.STATIC_DIR)
|
|
891
|
-
if CONFIG.orchestrator.STATIC_DIR
|
|
892
|
-
else Path(__file__).parent / "static"
|
|
888
|
+
Path(CONFIG.orchestrator.STATIC_DIR) if CONFIG.orchestrator.STATIC_DIR else Path(__file__).parent / "static"
|
|
893
889
|
)
|
|
894
890
|
return FileResponse(static_path / "review_sequences.html")
|
|
895
891
|
|
|
@@ -899,13 +895,12 @@ static_dir = Path(CONFIG.orchestrator.STATIC_DIR) if CONFIG.orchestrator.STATIC_
|
|
|
899
895
|
if static_dir and static_dir.exists():
|
|
900
896
|
app.mount("/static", StaticFiles(directory=str(static_dir)), name="static")
|
|
901
897
|
|
|
898
|
+
|
|
902
899
|
# Serve review_common.js from static directory
|
|
903
900
|
@app.get("/review_common.js")
|
|
904
901
|
async def serve_review_common_js() -> FileResponse:
|
|
905
902
|
"""Serve review_common.js from static directory."""
|
|
906
903
|
static_path = (
|
|
907
|
-
Path(CONFIG.orchestrator.STATIC_DIR)
|
|
908
|
-
if CONFIG.orchestrator.STATIC_DIR
|
|
909
|
-
else Path(__file__).parent / "static"
|
|
904
|
+
Path(CONFIG.orchestrator.STATIC_DIR) if CONFIG.orchestrator.STATIC_DIR else Path(__file__).parent / "static"
|
|
910
905
|
)
|
|
911
906
|
return FileResponse(static_path / "review_common.js", media_type="application/javascript")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Pipeline construction using PipelineBuilder pattern.
|
|
2
2
|
|
|
3
|
-
This module constructs the complete photo
|
|
3
|
+
This module constructs the complete photo stack finding pipeline using the
|
|
4
4
|
new port-based orchestration system. All 8 stages are wired together via
|
|
5
5
|
Channel connections based on their InputPort/OutputPort declarations.
|
|
6
6
|
"""
|
|
@@ -19,7 +19,6 @@ from utils import (
|
|
|
19
19
|
ComputePerceptualHash,
|
|
20
20
|
ComputePerceptualMatch,
|
|
21
21
|
ComputeShaBins,
|
|
22
|
-
ComputeTemplates,
|
|
23
22
|
ComputeTemplateSimilarity,
|
|
24
23
|
ComputeVersions,
|
|
25
24
|
InputPort,
|
|
@@ -31,18 +30,21 @@ from .pipeline_orchestrator import PipelineOrchestrator
|
|
|
31
30
|
|
|
32
31
|
|
|
33
32
|
def build_pipeline(source_dir: Path) -> PipelineOrchestrator:
|
|
34
|
-
"""Build the complete photo
|
|
35
|
-
|
|
36
|
-
Constructs a pipeline graph with 8
|
|
37
|
-
1. ComputeShaBins - Hash files
|
|
38
|
-
2. ComputeIdentical - Find byte-identical duplicates
|
|
39
|
-
3.
|
|
40
|
-
4.
|
|
41
|
-
5.
|
|
42
|
-
6.
|
|
43
|
-
7.
|
|
44
|
-
8.
|
|
45
|
-
|
|
33
|
+
"""Build the complete photo stack finding pipeline.
|
|
34
|
+
|
|
35
|
+
Constructs a pipeline graph with 7-8 stages connected via ports:
|
|
36
|
+
1. ComputeShaBins - Hash files, bin by SHA256, extract templates
|
|
37
|
+
2. ComputeIdentical - Find byte-identical duplicates, output template bins
|
|
38
|
+
3. ComputeVersions - Detect version patterns in filenames
|
|
39
|
+
4. ComputeTemplateSimilarity - Match photos with similar templates
|
|
40
|
+
5. ComputeIndices - Find sequences with overlapping indices
|
|
41
|
+
6. ComputePerceptualHash - Compute perceptual hashes and bin
|
|
42
|
+
7. ComputePerceptualMatch - Match photos by perceptual hash similarity
|
|
43
|
+
8. ComputeBenchmarks - (Optional, controlled by CONFIG.benchmark.ENABLED)
|
|
44
|
+
|
|
45
|
+
Note: ComputeTemplates stage has been merged into ComputeIdentical.
|
|
46
|
+
Template extraction now happens during SHA binning in ComputeShaBins,
|
|
47
|
+
and template binning happens in ComputeIdentical's finalise() method.
|
|
46
48
|
|
|
47
49
|
Args:
|
|
48
50
|
source_dir: Root directory containing photos to process
|
|
@@ -60,17 +62,13 @@ def build_pipeline(source_dir: Path) -> PipelineOrchestrator:
|
|
|
60
62
|
# SHA256 Hashing and Binning
|
|
61
63
|
sha_bins_stage = ComputeShaBins(source_path=source_dir)
|
|
62
64
|
|
|
63
|
-
# Identical Files Detection
|
|
65
|
+
# Identical Files Detection (outputs template bins)
|
|
64
66
|
identical_stage = ComputeIdentical()
|
|
65
67
|
Channel(sha_bins_stage.sha_bins_o, identical_stage.sha_bins_i)
|
|
66
68
|
|
|
67
|
-
#
|
|
68
|
-
templates_stage = ComputeTemplates()
|
|
69
|
-
Channel(identical_stage.nonidentical_o, templates_stage.nonidentical_photos_i)
|
|
70
|
-
|
|
71
|
-
# Version Detection
|
|
69
|
+
# Version Detection (receives template bins directly from ComputeIdentical)
|
|
72
70
|
versions_stage = ComputeVersions()
|
|
73
|
-
Channel(
|
|
71
|
+
Channel(identical_stage.nonidentical_o, versions_stage.template_bins_i)
|
|
74
72
|
|
|
75
73
|
# Template Similarity
|
|
76
74
|
template_similarity_stage = ComputeTemplateSimilarity()
|
{photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/orchestrator/orchestrator_runner.py
RENAMED
|
@@ -68,25 +68,25 @@ def get_os_subdir() -> str:
|
|
|
68
68
|
|
|
69
69
|
Returns:
|
|
70
70
|
OS-specific subdirectory name:
|
|
71
|
-
- '
|
|
71
|
+
- 'windows' for Windows
|
|
72
72
|
- 'linux' for Linux
|
|
73
73
|
- 'darwin' for macOS
|
|
74
74
|
- Platform name for others
|
|
75
75
|
|
|
76
76
|
Example:
|
|
77
|
-
work_dir = source_parent / "
|
|
78
|
-
# Windows: .../
|
|
79
|
-
# Linux: .../
|
|
80
|
-
# macOS: .../
|
|
77
|
+
work_dir = source_parent / "photo_stack_finder" / get_os_subdir()
|
|
78
|
+
# Windows: .../photo_stack_finder/windows/
|
|
79
|
+
# Linux: .../photo_stack_finder/linux/
|
|
80
|
+
# macOS: .../photo_stack_finder/darwin/
|
|
81
81
|
"""
|
|
82
82
|
platform = sys.platform
|
|
83
83
|
if platform.startswith("win"):
|
|
84
|
-
return "
|
|
84
|
+
return "windows"
|
|
85
85
|
if platform.startswith("linux"):
|
|
86
86
|
return "linux"
|
|
87
87
|
if platform.startswith("darwin"):
|
|
88
88
|
return "darwin"
|
|
89
|
-
return platform # Fallback for other platforms
|
|
89
|
+
return platform # Fallback for other platforms # Fallback for other platforms
|
|
90
90
|
|
|
91
91
|
|
|
92
92
|
@dataclass
|
|
@@ -468,7 +468,7 @@ class OrchestratorRunner:
|
|
|
468
468
|
# Default work_dir if not provided: OS-specific subdirectory
|
|
469
469
|
source_path = Path(CONFIG.paths.SOURCE_DIR)
|
|
470
470
|
os_subdir = get_os_subdir()
|
|
471
|
-
CONFIG.paths.WORK_DIR = str(source_path.parent / "
|
|
471
|
+
CONFIG.paths.WORK_DIR = str(source_path.parent / "photo_stack_finder" / os_subdir)
|
|
472
472
|
|
|
473
473
|
# Create work directory if it doesn't exist
|
|
474
474
|
CONFIG.paths.work_dir.mkdir(parents=True, exist_ok=True)
|
|
@@ -483,6 +483,9 @@ class OrchestratorRunner:
|
|
|
483
483
|
if "debug_mode" in config:
|
|
484
484
|
CONFIG.processing.DEBUG_MODE = config["debug_mode"]
|
|
485
485
|
|
|
486
|
+
if "skip_byte_identical" in config:
|
|
487
|
+
CONFIG.processing.SKIP_BYTE_IDENTICAL = config["skip_byte_identical"]
|
|
488
|
+
|
|
486
489
|
# Update gate thresholds if provided
|
|
487
490
|
if config.get("gate_thresholds"):
|
|
488
491
|
assert CONFIG.processing.GATE_THRESHOLDS is not None
|
{photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/orchestrator/static/orchestrator.html
RENAMED
|
@@ -120,6 +120,12 @@
|
|
|
120
120
|
Debug Mode (sequential processing)
|
|
121
121
|
</label>
|
|
122
122
|
</div>
|
|
123
|
+
<div class="form-group">
|
|
124
|
+
<label for="skip-byte-identical">
|
|
125
|
+
<input type="checkbox" id="skip-byte-identical" name="skip_byte_identical" checked>
|
|
126
|
+
Skip byte-identical detection (trust SHA256 uniqueness)
|
|
127
|
+
</label>
|
|
128
|
+
</div>
|
|
123
129
|
</div>
|
|
124
130
|
|
|
125
131
|
<div class="option-group">
|
{photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/orchestrator/static/orchestrator.js
RENAMED
|
@@ -313,6 +313,7 @@ function populateForm(config) {
|
|
|
313
313
|
document.getElementById('max-workers').value = config.max_workers || '';
|
|
314
314
|
document.getElementById('batch-size').value = config.batch_size || '';
|
|
315
315
|
document.getElementById('debug-mode').checked = config.debug_mode || false;
|
|
316
|
+
document.getElementById('skip-byte-identical').checked = config.skip_byte_identical !== false; // Default true
|
|
316
317
|
|
|
317
318
|
document.getElementById('comparison-method').value = config.comparison_method || 'SSIM';
|
|
318
319
|
document.getElementById('ssim-threshold').value = config.gate_thresholds?.SSIM || 0.95;
|
|
@@ -369,6 +370,7 @@ async function handleFormSubmit(event) {
|
|
|
369
370
|
max_workers: maxWorkers ? parseInt(maxWorkers) : null,
|
|
370
371
|
batch_size: batchSize ? parseInt(batchSize) : null,
|
|
371
372
|
debug_mode: formData.get('debug_mode') === 'on',
|
|
373
|
+
skip_byte_identical: formData.get('skip_byte_identical') === 'on',
|
|
372
374
|
comparison_method: formData.get('comparison_method'),
|
|
373
375
|
gate_thresholds: {
|
|
374
376
|
SSIM: parseFloat(formData.get('ssim_threshold')),
|
|
@@ -40,7 +40,7 @@ if version.parse(scipy.__version__) < version.parse(MINIMUM_SCIPY_VERSION):
|
|
|
40
40
|
|
|
41
41
|
# Version info
|
|
42
42
|
__version__ = "1.0.0"
|
|
43
|
-
__author__ = "Photo
|
|
43
|
+
__author__ = "Photo Stack Finder Team"
|
|
44
44
|
__description__ = "Distribution separation and overlap metrics with pluggable estimators"
|
|
45
45
|
|
|
46
46
|
# Default metric suite for compute_suite()
|
|
@@ -43,7 +43,7 @@ from .structural_methods import HOGMethod, MultiScaleSSIMMethod, SSIMMethod
|
|
|
43
43
|
|
|
44
44
|
# Version information
|
|
45
45
|
__version__ = "1.0.0"
|
|
46
|
-
__author__ = "Photo
|
|
46
|
+
__author__ = "Photo Stack Finder Team"
|
|
47
47
|
__description__ = "Image similarity methods with integrated caching and factory pattern"
|
|
48
48
|
|
|
49
49
|
|
{photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/photo_stack_finder.egg-info/PKG-INFO
RENAMED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: photo-stack-finder
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.8
|
|
4
4
|
Summary: Photo deduplication using perceptual hashing and sequence detection
|
|
5
5
|
Author: Geoff Barrett
|
|
6
6
|
Maintainer: Geoff Barrett
|
|
7
|
-
License: AGPL-3.0-or-later
|
|
7
|
+
License-Expression: AGPL-3.0-or-later
|
|
8
8
|
Project-URL: Homepage, https://github.com/gbarrett28/photo_dedup
|
|
9
9
|
Project-URL: Repository, https://github.com/gbarrett28/photo_dedup
|
|
10
10
|
Project-URL: Issues, https://github.com/gbarrett28/photo_dedup/issues
|
|
@@ -12,7 +12,6 @@ Project-URL: Discussions, https://github.com/gbarrett28/photo_dedup/discussions
|
|
|
12
12
|
Keywords: photo,deduplication,perceptual-hashing,image-processing
|
|
13
13
|
Classifier: Development Status :: 4 - Beta
|
|
14
14
|
Classifier: Intended Audience :: End Users/Desktop
|
|
15
|
-
Classifier: License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)
|
|
16
15
|
Classifier: Programming Language :: Python :: 3
|
|
17
16
|
Classifier: Programming Language :: Python :: 3.11
|
|
18
17
|
Classifier: Programming Language :: Python :: 3.12
|
{photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/photo_stack_finder.egg-info/SOURCES.txt
RENAMED
|
@@ -55,7 +55,6 @@ src/utils/compute_perceptual_hash.py
|
|
|
55
55
|
src/utils/compute_perceptual_match.py
|
|
56
56
|
src/utils/compute_sha_bins.py
|
|
57
57
|
src/utils/compute_template_similarity.py
|
|
58
|
-
src/utils/compute_templates.py
|
|
59
58
|
src/utils/compute_versions.py
|
|
60
59
|
src/utils/config.py
|
|
61
60
|
src/utils/data_io.py
|
|
@@ -75,6 +74,7 @@ src/utils/review_utils.py
|
|
|
75
74
|
src/utils/sequence.py
|
|
76
75
|
src/utils/sequence_clustering.py
|
|
77
76
|
src/utils/template.py
|
|
77
|
+
src/utils/template_parsing.py
|
|
78
78
|
tests/test_app_endpoints_comprehensive.py
|
|
79
79
|
tests/test_comparison_methods_comprehensive.py
|
|
80
80
|
tests/test_compute_benchmarks_comprehensive.py
|
|
@@ -84,7 +84,6 @@ tests/test_compute_indices_comprehensive.py
|
|
|
84
84
|
tests/test_compute_perceptual_hash_comprehensive.py
|
|
85
85
|
tests/test_compute_perceptual_match_comprehensive.py
|
|
86
86
|
tests/test_compute_sha_bins_comprehensive.py
|
|
87
|
-
tests/test_compute_templates_comprehensive.py
|
|
88
87
|
tests/test_compute_versions_comprehensive.py
|
|
89
88
|
tests/test_orchestrator_comprehensive.py
|
|
90
89
|
tests/test_photo_file_pixel_array.py
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""Photo
|
|
2
|
+
"""Photo Stack Finderlication Orchestrator - Web Interface Entry Point.
|
|
3
3
|
|
|
4
|
-
This provides a web-based interface for the photo
|
|
4
|
+
This provides a web-based interface for the photo stack finding pipeline.
|
|
5
5
|
|
|
6
6
|
Usage:
|
|
7
7
|
python orchestrate.py
|
|
@@ -38,10 +38,10 @@ def build_arg_parser() -> argparse.ArgumentParser:
|
|
|
38
38
|
Configured ArgumentParser instance
|
|
39
39
|
"""
|
|
40
40
|
parser = argparse.ArgumentParser(
|
|
41
|
-
description="Photo
|
|
41
|
+
description="Photo Stack Finder Orchestrator - Web Interface",
|
|
42
42
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
43
43
|
epilog="""
|
|
44
|
-
This is a web interface for the photo
|
|
44
|
+
This is a web interface for the photo stack finding pipeline.
|
|
45
45
|
|
|
46
46
|
For the web interface, just run:
|
|
47
47
|
- python orchestrate.py
|
|
@@ -277,11 +277,13 @@ def main() -> None:
|
|
|
277
277
|
|
|
278
278
|
# Add platform-specific instructions
|
|
279
279
|
if is_linux:
|
|
280
|
-
lines.extend(
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
280
|
+
lines.extend(
|
|
281
|
+
[
|
|
282
|
+
"To access the web interface:",
|
|
283
|
+
f" Open your browser to: {url}",
|
|
284
|
+
"",
|
|
285
|
+
]
|
|
286
|
+
)
|
|
285
287
|
|
|
286
288
|
lines.append("Press Ctrl+C to stop the server")
|
|
287
289
|
print_banner("", lines)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Utilities for photo
|
|
1
|
+
"""Utilities for photo stack finding."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
@@ -26,7 +26,6 @@ from .compute_perceptual_hash import ComputePerceptualHash
|
|
|
26
26
|
from .compute_perceptual_match import ComputePerceptualMatch
|
|
27
27
|
from .compute_sha_bins import ComputeShaBins
|
|
28
28
|
from .compute_template_similarity import ComputeTemplateSimilarity
|
|
29
|
-
from .compute_templates import ComputeTemplates
|
|
30
29
|
from .compute_versions import ComputeVersions
|
|
31
30
|
|
|
32
31
|
# Config module exports
|
|
@@ -109,6 +108,9 @@ from .template import (
|
|
|
109
108
|
partial_format,
|
|
110
109
|
)
|
|
111
110
|
|
|
111
|
+
# Template parsing module exports (extract_template not exported - internal use only)
|
|
112
|
+
# INDEX_T exported via sequence module
|
|
113
|
+
|
|
112
114
|
__all__ = [
|
|
113
115
|
# Config
|
|
114
116
|
"CONFIG",
|
|
@@ -136,7 +138,6 @@ __all__ = [
|
|
|
136
138
|
"ComputePerceptualMatch",
|
|
137
139
|
"ComputeShaBins",
|
|
138
140
|
"ComputeTemplateSimilarity",
|
|
139
|
-
"ComputeTemplates",
|
|
140
141
|
"ComputeVersions",
|
|
141
142
|
# Configuration
|
|
142
143
|
"Config",
|
|
@@ -291,8 +291,8 @@ class AspectRatioGate(BaseGate):
|
|
|
291
291
|
"""
|
|
292
292
|
# Get aspect ratios (may trigger lazy dimension extraction)
|
|
293
293
|
# This may load pixels if dimensions not yet cached
|
|
294
|
-
ratio1 = img1.
|
|
295
|
-
ratio2 = img2.
|
|
294
|
+
ratio1 = img1._photo.aspect_ratio
|
|
295
|
+
ratio2 = img2._photo.aspect_ratio
|
|
296
296
|
|
|
297
297
|
# Normalize to portrait orientation (AR < 1.0)
|
|
298
298
|
# This allows matching landscape ↔ portrait photos
|
|
@@ -372,8 +372,14 @@ class GateSequence:
|
|
|
372
372
|
# MethodGate: check if pixels needed (cache miss detection)
|
|
373
373
|
# Only load pixels if we'll actually need them
|
|
374
374
|
# Note: cache keys are (method_name, rotation) tuples, always tuples even for rotation=0
|
|
375
|
-
will_need_pixels1 = (photo1.id, 0) not in gate.cache and (
|
|
376
|
-
|
|
375
|
+
will_need_pixels1 = (photo1.id, 0) not in gate.cache and (
|
|
376
|
+
gate._name,
|
|
377
|
+
0,
|
|
378
|
+
) not in photo1.cache
|
|
379
|
+
will_need_pixels2 = (photo2.id, 0) not in gate.cache and (
|
|
380
|
+
gate._name,
|
|
381
|
+
0,
|
|
382
|
+
) not in photo2.cache
|
|
377
383
|
|
|
378
384
|
if will_need_pixels1 or will_need_pixels2:
|
|
379
385
|
# Cache miss detected - load pixels if not already loaded
|
|
@@ -430,29 +436,24 @@ class GateSequence:
|
|
|
430
436
|
# Flexible ImageData management: create contexts only for photos that don't have pre-created ImageData
|
|
431
437
|
# Case 1: Both provided - use directly
|
|
432
438
|
if ref_img is not None and cand_img is not None:
|
|
433
|
-
return self._compare_with_rotation_impl(
|
|
434
|
-
reference, candidate, ref_img, cand_img, short_circuit
|
|
435
|
-
)
|
|
439
|
+
return self._compare_with_rotation_impl(reference, candidate, ref_img, cand_img, short_circuit)
|
|
436
440
|
|
|
437
441
|
# Case 2: Only ref provided - create context for candidate
|
|
438
442
|
if ref_img is not None and cand_img is None:
|
|
439
443
|
with candidate.image_data() as cand_img_ctx:
|
|
440
|
-
return self._compare_with_rotation_impl(
|
|
441
|
-
reference, candidate, ref_img, cand_img_ctx, short_circuit
|
|
442
|
-
)
|
|
444
|
+
return self._compare_with_rotation_impl(reference, candidate, ref_img, cand_img_ctx, short_circuit)
|
|
443
445
|
|
|
444
446
|
# Case 3: Only cand provided - create context for reference
|
|
445
447
|
if ref_img is None and cand_img is not None:
|
|
446
448
|
with reference.image_data() as ref_img_ctx:
|
|
447
|
-
return self._compare_with_rotation_impl(
|
|
448
|
-
reference, candidate, ref_img_ctx, cand_img, short_circuit
|
|
449
|
-
)
|
|
449
|
+
return self._compare_with_rotation_impl(reference, candidate, ref_img_ctx, cand_img, short_circuit)
|
|
450
450
|
|
|
451
451
|
# Case 4: Neither provided - create contexts for both (original pattern)
|
|
452
|
-
with
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
452
|
+
with (
|
|
453
|
+
reference.image_data() as ref_img_new,
|
|
454
|
+
candidate.image_data() as cand_img_new,
|
|
455
|
+
):
|
|
456
|
+
return self._compare_with_rotation_impl(reference, candidate, ref_img_new, cand_img_new, short_circuit)
|
|
456
457
|
|
|
457
458
|
def _compare_with_rotation_impl(
|
|
458
459
|
self,
|
|
@@ -508,7 +509,13 @@ class GateSequence:
|
|
|
508
509
|
# Second attempt: try 180° rotation ONLY if first failed
|
|
509
510
|
if not overall_pass:
|
|
510
511
|
ref_rotation_180 = ref_norm_rotation + 180
|
|
511
|
-
|
|
512
|
+
(
|
|
513
|
+
scores_180,
|
|
514
|
+
overall_pass_180,
|
|
515
|
+
final_gate_score_180,
|
|
516
|
+
ref_pixels,
|
|
517
|
+
cand_pixels,
|
|
518
|
+
) = self._attempt_comparison(
|
|
512
519
|
reference,
|
|
513
520
|
candidate,
|
|
514
521
|
ref_img,
|
|
@@ -539,7 +546,13 @@ class GateSequence:
|
|
|
539
546
|
ref_pixels: npt.NDArray[np.uint8] | None,
|
|
540
547
|
cand_pixels: npt.NDArray[np.uint8] | None,
|
|
541
548
|
short_circuit: bool,
|
|
542
|
-
) -> tuple[
|
|
549
|
+
) -> tuple[
|
|
550
|
+
dict[str, float],
|
|
551
|
+
bool,
|
|
552
|
+
float,
|
|
553
|
+
npt.NDArray[np.uint8] | None,
|
|
554
|
+
npt.NDArray[np.uint8] | None,
|
|
555
|
+
]:
|
|
543
556
|
"""Attempt comparison at specific rotation angles.
|
|
544
557
|
|
|
545
558
|
Args:
|
|
@@ -569,11 +582,17 @@ class GateSequence:
|
|
|
569
582
|
elif isinstance(gate, MethodGate):
|
|
570
583
|
# Check if we need to load pixels (cache miss detection)
|
|
571
584
|
# Cache keys are always (method_name, rotation) tuples
|
|
572
|
-
will_need_ref_pixels = (
|
|
585
|
+
will_need_ref_pixels = (
|
|
586
|
+
reference.id,
|
|
587
|
+
ref_rotation,
|
|
588
|
+
) not in gate.cache and (
|
|
573
589
|
gate._name,
|
|
574
590
|
ref_rotation,
|
|
575
591
|
) not in reference.cache
|
|
576
|
-
will_need_cand_pixels = (
|
|
592
|
+
will_need_cand_pixels = (
|
|
593
|
+
candidate.id,
|
|
594
|
+
cand_rotation,
|
|
595
|
+
) not in gate.cache and (
|
|
577
596
|
gate._name,
|
|
578
597
|
cand_rotation,
|
|
579
598
|
) not in candidate.cache
|