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.
Files changed (96) hide show
  1. {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/PKG-INFO +2 -3
  2. {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/pyproject.toml +2 -3
  3. {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/orchestrator/__init__.py +2 -2
  4. {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/orchestrator/app.py +6 -11
  5. {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/orchestrator/build_pipeline.py +19 -21
  6. {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/orchestrator/orchestrator_runner.py +11 -8
  7. {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/orchestrator/static/orchestrator.html +6 -0
  8. {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/orchestrator/static/orchestrator.js +2 -0
  9. {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/overlap_metrics/__init__.py +1 -1
  10. {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/photo_compare/__init__.py +1 -1
  11. {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/photo_stack_finder.egg-info/PKG-INFO +2 -3
  12. {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/photo_stack_finder.egg-info/SOURCES.txt +1 -2
  13. {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/scripts/orchestrate.py +11 -9
  14. {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/utils/__init__.py +4 -3
  15. {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/utils/comparison_gates.py +40 -21
  16. {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/utils/compute_identical.py +92 -22
  17. {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/utils/compute_sha_bins.py +62 -20
  18. {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/utils/config.py +8 -5
  19. {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/utils/logger.py +2 -2
  20. {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/utils/models.py +2 -2
  21. {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/utils/photo_file.py +90 -91
  22. {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/utils/review_utils.py +10 -19
  23. {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/utils/sequence.py +10 -8
  24. {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/utils/sequence_clustering.py +1 -1
  25. photo_stack_finder-0.1.8/src/utils/template_parsing.py +71 -0
  26. {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/tests/test_compute_identical_comprehensive.py +36 -20
  27. {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/tests/test_compute_indices_comprehensive.py +8 -0
  28. {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/tests/test_compute_perceptual_hash_comprehensive.py +8 -0
  29. {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/tests/test_template_similarity_comprehensive.py +8 -0
  30. photo_stack_finder-0.1.7/src/utils/compute_templates.py +0 -137
  31. photo_stack_finder-0.1.7/tests/test_compute_templates_comprehensive.py +0 -386
  32. {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/LICENSE +0 -0
  33. {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/README.md +0 -0
  34. {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/setup.cfg +0 -0
  35. {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/orchestrator/pipeline_builder.py +0 -0
  36. {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/orchestrator/pipeline_orchestrator.py +0 -0
  37. {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/orchestrator/py.typed +0 -0
  38. {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/orchestrator/review_persistence.py +0 -0
  39. {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/orchestrator/static/favicon.svg +0 -0
  40. {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/orchestrator/static/orchestrator.css +0 -0
  41. {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/orchestrator/static/review_common.js +0 -0
  42. {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/orchestrator/static/review_identical.html +0 -0
  43. {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/orchestrator/static/review_sequences.html +0 -0
  44. {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/overlap_metrics/config.py +0 -0
  45. {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/overlap_metrics/core.py +0 -0
  46. {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/overlap_metrics/estimators.py +0 -0
  47. {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/overlap_metrics/metrics.py +0 -0
  48. {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/overlap_metrics/registry.py +0 -0
  49. {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/overlap_metrics/utils.py +0 -0
  50. {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/photo_compare/base.py +0 -0
  51. {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/photo_compare/config.py +0 -0
  52. {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/photo_compare/distance.py +0 -0
  53. {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/photo_compare/feature_methods.py +0 -0
  54. {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/photo_compare/file_hash.py +0 -0
  55. {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/photo_compare/hash_methods.py +0 -0
  56. {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/photo_compare/histogram_methods.py +0 -0
  57. {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/photo_compare/pixel_methods.py +0 -0
  58. {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/photo_compare/py.typed +0 -0
  59. {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/photo_compare/structural_methods.py +0 -0
  60. {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/photo_compare/types.py +0 -0
  61. {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/photo_stack_finder.egg-info/dependency_links.txt +0 -0
  62. {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/photo_stack_finder.egg-info/entry_points.txt +0 -0
  63. {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/photo_stack_finder.egg-info/requires.txt +0 -0
  64. {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/photo_stack_finder.egg-info/top_level.txt +0 -0
  65. {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/utils/base_pipeline_stage.py +0 -0
  66. {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/utils/base_ports.py +0 -0
  67. {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/utils/benchmark_utils.py +0 -0
  68. {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/utils/channel.py +0 -0
  69. {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/utils/compute_benchmarks.py +0 -0
  70. {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/utils/compute_indices.py +0 -0
  71. {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/utils/compute_perceptual_hash.py +0 -0
  72. {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/utils/compute_perceptual_match.py +0 -0
  73. {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/utils/compute_template_similarity.py +0 -0
  74. {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/utils/compute_versions.py +0 -0
  75. {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/utils/data_io.py +0 -0
  76. {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/utils/graph_context.py +0 -0
  77. {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/utils/pipeline_graph.py +0 -0
  78. {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/utils/pipeline_stage.py +0 -0
  79. {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/utils/plot_helpers.py +0 -0
  80. {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/utils/ports.py +0 -0
  81. {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/utils/progress.py +0 -0
  82. {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/utils/py.typed +0 -0
  83. {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/utils/report_builder.py +0 -0
  84. {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/utils/review_types.py +0 -0
  85. {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/src/utils/template.py +0 -0
  86. {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/tests/test_app_endpoints_comprehensive.py +0 -0
  87. {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/tests/test_comparison_methods_comprehensive.py +0 -0
  88. {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/tests/test_compute_benchmarks_comprehensive.py +0 -0
  89. {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/tests/test_compute_benchmarks_design.py +0 -0
  90. {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/tests/test_compute_perceptual_match_comprehensive.py +0 -0
  91. {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/tests/test_compute_sha_bins_comprehensive.py +0 -0
  92. {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/tests/test_compute_versions_comprehensive.py +0 -0
  93. {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/tests/test_orchestrator_comprehensive.py +0 -0
  94. {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/tests/test_photo_file_pixel_array.py +0 -0
  95. {photo_stack_finder-0.1.7 → photo_stack_finder-0.1.8}/tests/test_rotation_aware_comparison.py +0 -0
  96. {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.7
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.7"
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 = {text = "AGPL-3.0-or-later"}
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 deduplication workflow.
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 deduplication system. It includes:
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 deduplication.
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 Dedup Orchestrator", lifespan=lifespan_manager)
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 deduplication pipeline using the
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 deduplication pipeline.
35
-
36
- Constructs a pipeline graph with 8-9 stages connected via ports:
37
- 1. ComputeShaBins - Hash files and bin by SHA256
38
- 2. ComputeIdentical - Find byte-identical duplicates
39
- 3. ComputeTemplates - Bin photos by filename template
40
- 4. ComputeVersions - Detect version patterns in filenames
41
- 5. ComputeTemplateSimilarity - Match photos with similar templates
42
- 6. ComputeIndices - Find sequences with overlapping indices
43
- 7. ComputePerceptualHash - Compute perceptual hashes and bin
44
- 8. ComputePerceptualMatch - Match photos by perceptual hash similarity
45
- 9. ComputeBenchmarks - (Optional, controlled by CONFIG.benchmark.ENABLED)
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
- # Template Binning
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(templates_stage.template_bins_o, versions_stage.template_bins_i)
71
+ Channel(identical_stage.nonidentical_o, versions_stage.template_bins_i)
74
72
 
75
73
  # Template Similarity
76
74
  template_similarity_stage = ComputeTemplateSimilarity()
@@ -68,25 +68,25 @@ def get_os_subdir() -> str:
68
68
 
69
69
  Returns:
70
70
  OS-specific subdirectory name:
71
- - 'window' for Windows
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 / "photo_dedup" / get_os_subdir()
78
- # Windows: .../photo_dedup/window/
79
- # Linux: .../photo_dedup/linux/
80
- # macOS: .../photo_dedup/darwin/
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 "window"
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 / "photo_dedup" / os_subdir)
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
@@ -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">
@@ -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 Deduplication Team"
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 Deduplication Team"
46
+ __author__ = "Photo Stack Finder Team"
47
47
  __description__ = "Image similarity methods with integrated caching and factory pattern"
48
48
 
49
49
 
@@ -1,10 +1,10 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: photo-stack-finder
3
- Version: 0.1.7
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
@@ -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 Deduplication Orchestrator - Web Interface Entry Point.
2
+ """Photo Stack Finderlication Orchestrator - Web Interface Entry Point.
3
3
 
4
- This provides a web-based interface for the photo deduplication pipeline.
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 Dedup Orchestrator - Web Interface",
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 deduplication pipeline.
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
- "To access the web interface:",
282
- f" Open your browser to: {url}",
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 deduplication."""
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.get_aspect_ratio()
295
- ratio2 = img2.get_aspect_ratio()
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 (gate._name, 0) not in photo1.cache
376
- will_need_pixels2 = (photo2.id, 0) not in gate.cache and (gate._name, 0) not in photo2.cache
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 reference.image_data() as ref_img_new, candidate.image_data() as cand_img_new:
453
- return self._compare_with_rotation_impl(
454
- reference, candidate, ref_img_new, cand_img_new, short_circuit
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
- scores_180, overall_pass_180, final_gate_score_180, ref_pixels, cand_pixels = self._attempt_comparison(
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[dict[str, float], bool, float, npt.NDArray[np.uint8] | None, npt.NDArray[np.uint8] | None]:
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 = (reference.id, ref_rotation) not in gate.cache and (
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 = (candidate.id, cand_rotation) not in gate.cache and (
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