photo-stack-finder 0.1.7__py3-none-any.whl → 0.1.8__py3-none-any.whl

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 (68) hide show
  1. orchestrator/__init__.py +2 -2
  2. orchestrator/app.py +6 -11
  3. orchestrator/build_pipeline.py +19 -21
  4. orchestrator/orchestrator_runner.py +11 -8
  5. orchestrator/pipeline_builder.py +126 -126
  6. orchestrator/pipeline_orchestrator.py +604 -604
  7. orchestrator/review_persistence.py +162 -162
  8. orchestrator/static/orchestrator.css +76 -76
  9. orchestrator/static/orchestrator.html +11 -5
  10. orchestrator/static/orchestrator.js +3 -1
  11. overlap_metrics/__init__.py +1 -1
  12. overlap_metrics/config.py +135 -135
  13. overlap_metrics/core.py +284 -284
  14. overlap_metrics/estimators.py +292 -292
  15. overlap_metrics/metrics.py +307 -307
  16. overlap_metrics/registry.py +99 -99
  17. overlap_metrics/utils.py +104 -104
  18. photo_compare/__init__.py +1 -1
  19. photo_compare/base.py +285 -285
  20. photo_compare/config.py +225 -225
  21. photo_compare/distance.py +15 -15
  22. photo_compare/feature_methods.py +173 -173
  23. photo_compare/file_hash.py +29 -29
  24. photo_compare/hash_methods.py +99 -99
  25. photo_compare/histogram_methods.py +118 -118
  26. photo_compare/pixel_methods.py +58 -58
  27. photo_compare/structural_methods.py +104 -104
  28. photo_compare/types.py +28 -28
  29. {photo_stack_finder-0.1.7.dist-info → photo_stack_finder-0.1.8.dist-info}/METADATA +21 -22
  30. photo_stack_finder-0.1.8.dist-info/RECORD +75 -0
  31. scripts/orchestrate.py +12 -10
  32. utils/__init__.py +4 -3
  33. utils/base_pipeline_stage.py +171 -171
  34. utils/base_ports.py +176 -176
  35. utils/benchmark_utils.py +823 -823
  36. utils/channel.py +74 -74
  37. utils/comparison_gates.py +40 -21
  38. utils/compute_benchmarks.py +355 -355
  39. utils/compute_identical.py +94 -24
  40. utils/compute_indices.py +235 -235
  41. utils/compute_perceptual_hash.py +127 -127
  42. utils/compute_perceptual_match.py +240 -240
  43. utils/compute_sha_bins.py +64 -20
  44. utils/compute_template_similarity.py +1 -1
  45. utils/compute_versions.py +483 -483
  46. utils/config.py +8 -5
  47. utils/data_io.py +83 -83
  48. utils/graph_context.py +44 -44
  49. utils/logger.py +2 -2
  50. utils/models.py +2 -2
  51. utils/photo_file.py +90 -91
  52. utils/pipeline_graph.py +334 -334
  53. utils/pipeline_stage.py +408 -408
  54. utils/plot_helpers.py +123 -123
  55. utils/ports.py +136 -136
  56. utils/progress.py +415 -415
  57. utils/report_builder.py +139 -139
  58. utils/review_types.py +55 -55
  59. utils/review_utils.py +10 -19
  60. utils/sequence.py +10 -8
  61. utils/sequence_clustering.py +1 -1
  62. utils/template.py +57 -57
  63. utils/template_parsing.py +71 -0
  64. photo_stack_finder-0.1.7.dist-info/RECORD +0 -74
  65. {photo_stack_finder-0.1.7.dist-info → photo_stack_finder-0.1.8.dist-info}/WHEEL +0 -0
  66. {photo_stack_finder-0.1.7.dist-info → photo_stack_finder-0.1.8.dist-info}/entry_points.txt +0 -0
  67. {photo_stack_finder-0.1.7.dist-info → photo_stack_finder-0.1.8.dist-info}/licenses/LICENSE +0 -0
  68. {photo_stack_finder-0.1.7.dist-info → photo_stack_finder-0.1.8.dist-info}/top_level.txt +0 -0
utils/channel.py CHANGED
@@ -1,74 +1,74 @@
1
- """Typed data channels connecting pipeline stages.
2
-
3
- A Channel connects an OutputPort to an InputPort, establishing a typed
4
- dependency edge in the pipeline graph. The Generic[T] type parameter
5
- ensures compile-time type safety - the output and input must have
6
- matching types.
7
-
8
- Architecture:
9
- - Auto-registration: Channels register themselves with the active graph
10
- - Type safety: Generic[T] enforces matching port types
11
- - Binding: Constructor automatically binds input port to output port
12
-
13
- Analogous to: Channels in SystemC, wires connecting modules in Verilog
14
- """
15
-
16
- from __future__ import annotations
17
-
18
- from typing import TypeVar
19
-
20
- from .base_ports import BaseChannel
21
- from .graph_context import get_active_graph
22
- from .ports import InputPort, OutputPort
23
-
24
- T = TypeVar("T")
25
-
26
-
27
- class Channel[T](BaseChannel):
28
- """Typed data connection between pipeline stages.
29
-
30
- Creates a dependency edge from producer (output port) to consumer
31
- (input port). The Generic[T] type parameter ensures type safety:
32
- both ports must handle the same data type.
33
-
34
- The channel automatically:
35
- 1. Binds the input port to the output port
36
- 2. Registers itself with the active PipelineGraph (if within PipelineBuilder context)
37
-
38
- Type parameter T specifies the data type flowing through this channel.
39
-
40
- Example:
41
- # Connect stages with type safety
42
- sha_bins_o: OutputPort[dict[str, list[str]]] = stage1.sha_bins_o
43
- sha_bins_input: InputPort[dict[str, list[str]]] = stage2.sha_bins_i
44
-
45
- # This will type-check correctly
46
- channel = Channel(sha_bins_o, sha_bins_input)
47
-
48
- # This would fail mypy type checking if types don't match
49
- # channel = Channel(different_type_output, sha_bins_input) # Error!
50
- """
51
-
52
- def __init__(self, port_o: OutputPort[T], port_i: InputPort[T]) -> None:
53
- """Create a typed channel connecting two ports.
54
-
55
- Args:
56
- port_o: The producer port (where data comes from)
57
- port_i: The consumer port (where data goes to)
58
-
59
- Note:
60
- If called within a PipelineBuilder context (i.e., when
61
- PipelineStage._graph is set), this channel will auto-register
62
- with the graph.
63
- """
64
- self.output = port_o
65
- self.input = port_i
66
-
67
- # Bind the input port to the output port
68
- # This establishes the data flow connection
69
- port_i.bind(port_o)
70
-
71
- # Auto-register this edge if we're within a PipelineBuilder context
72
- active_graph = get_active_graph()
73
- if active_graph is not None:
74
- active_graph.add_edge(self)
1
+ """Typed data channels connecting pipeline stages.
2
+
3
+ A Channel connects an OutputPort to an InputPort, establishing a typed
4
+ dependency edge in the pipeline graph. The Generic[T] type parameter
5
+ ensures compile-time type safety - the output and input must have
6
+ matching types.
7
+
8
+ Architecture:
9
+ - Auto-registration: Channels register themselves with the active graph
10
+ - Type safety: Generic[T] enforces matching port types
11
+ - Binding: Constructor automatically binds input port to output port
12
+
13
+ Analogous to: Channels in SystemC, wires connecting modules in Verilog
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from typing import TypeVar
19
+
20
+ from .base_ports import BaseChannel
21
+ from .graph_context import get_active_graph
22
+ from .ports import InputPort, OutputPort
23
+
24
+ T = TypeVar("T")
25
+
26
+
27
+ class Channel[T](BaseChannel):
28
+ """Typed data connection between pipeline stages.
29
+
30
+ Creates a dependency edge from producer (output port) to consumer
31
+ (input port). The Generic[T] type parameter ensures type safety:
32
+ both ports must handle the same data type.
33
+
34
+ The channel automatically:
35
+ 1. Binds the input port to the output port
36
+ 2. Registers itself with the active PipelineGraph (if within PipelineBuilder context)
37
+
38
+ Type parameter T specifies the data type flowing through this channel.
39
+
40
+ Example:
41
+ # Connect stages with type safety
42
+ sha_bins_o: OutputPort[dict[str, list[str]]] = stage1.sha_bins_o
43
+ sha_bins_input: InputPort[dict[str, list[str]]] = stage2.sha_bins_i
44
+
45
+ # This will type-check correctly
46
+ channel = Channel(sha_bins_o, sha_bins_input)
47
+
48
+ # This would fail mypy type checking if types don't match
49
+ # channel = Channel(different_type_output, sha_bins_input) # Error!
50
+ """
51
+
52
+ def __init__(self, port_o: OutputPort[T], port_i: InputPort[T]) -> None:
53
+ """Create a typed channel connecting two ports.
54
+
55
+ Args:
56
+ port_o: The producer port (where data comes from)
57
+ port_i: The consumer port (where data goes to)
58
+
59
+ Note:
60
+ If called within a PipelineBuilder context (i.e., when
61
+ PipelineStage._graph is set), this channel will auto-register
62
+ with the graph.
63
+ """
64
+ self.output = port_o
65
+ self.input = port_i
66
+
67
+ # Bind the input port to the output port
68
+ # This establishes the data flow connection
69
+ port_i.bind(port_o)
70
+
71
+ # Auto-register this edge if we're within a PipelineBuilder context
72
+ active_graph = get_active_graph()
73
+ if active_graph is not None:
74
+ active_graph.add_edge(self)
utils/comparison_gates.py CHANGED
@@ -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